轻松聘AI开发
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

16 KiB

S03「生产默认账号、简历隐私与 Actuator 隔离」独立代码复验及真实 MySQL/Docker 黑盒验收

日期:2026-09-15 分支:feat/s03-production-privacy 结论:FAIL —— 已复现具体缺陷(阶段 I-1:业务端口 Actuator 路径非 404),S03 不可冻结;报告包含下一轮定向整改指令。

1. Git 基线与工作区状态

  • 当前分支 feat/s03-production-privacy;HEAD f399060 feat: isolate and revoke authentication token sessions。
  • 暂存区为空;全部 S03 修改处于未提交(M/??)状态。
  • git diff --check 通过;git diff --name-only -- mydreamjob-server/src/main/resources/db/migration 为空(V1–V42 无改动)。
  • 唯一新增迁移:V43__disable_legacy_seed_accounts.sql。
  • 未执行 git add -A/commit/push/reset/restore/checkout/clean;未删除历史文件。

2. 独立代码审查(阶段 A)

项 结论 关键证据
A1 V43 迁移安全 PASS 3 条 UPDATE(无 DELETE/DROP/TRUNCATE);不按 id 匹配;每条同时匹配原始手机号+邮箱+公开 bcrypt 哈希+主角色+is_active=1;只置 is_active=0;不碰 user_roles;指纹被改的账号不命中;V1–V42 未修改
A2 管理员初始化装配 PASS 默认关闭 + 字面量 "true" 双重校验;手机号无默认值、非法即启动失败;已有活动 ADMIN 无副作用跳过;不设密码(password_hash=NULL)/不发 Token/不发短信/无公网接口;日志与异常文本固定且不含手机号(AdminBootstrapRunner.ERROR_INVALID_PHONE/ERROR_PHONE_IN_USE)
A3 事务与并发 PASS JdbcTemplate + REQUIRES_NEW 独立事务,users 与 user_roles 同事务回滚;刻意绕过 Hibernate 避免 SqlExceptionHelper 记录绑定手机号;异常被捕获只返回 CONFLICT,原始 cause 不传播;冲突后仅 isValidBootstrapAdmin 全条件匹配才幂等
A4 简历授权失败关闭 PASS CompanyResumeAccessPolicy:归属全部服务端推导;无记录/PRIVATE 按 PRIVATE;APPLIED_COMPANIES 严格白名单(SUB/VIEWED/INTERVIEW/HIRED,区分大小写,未知/空白/null/WITHDRAWN/REJECTED 全拒);VERIFIED_COMPANIES 要求 verified=true 且岗位归属;授权失败发生在文件读取前
A5 文件与响应安全 PASS 授权后读文件;Content-Disposition attachment+UTF-8、Cache-Control: no-store、Content-Type 白名单回退 octet-stream;响应不含 resumeUrl/storagePath/studentUserId
A6 Actuator 隔离(设计层面) PASS* 独立管理端口默认 127.0.0.1;仅暴露 health/info/metrics/prometheus;show-details/components=never;Prometheus 显式开启;不当 JWT 运维认证。*注:阶段 I-1 真实验收发现业务端口行为不达标,见第 12 节
A7 副作用测试真实性 PASS CompanyResumeDownloadIntegrationTest 行级字段快照(如 users: id/phone/email/role/is_active/is_verified/updated_at),非 COUNT/SUM;含反证测试(单行 status 变化必须被发现)与 15 表字段覆盖断言

3. 自动化独立复验(阶段 B)

命令 Tests Failures Errors Skipped 结果
mvn clean verify(实测,非沿用历史) 1834 0 0 0 BUILD SUCCESS
python docs\verify_openapi.py — — — — paths=176 operations=214,All checks passed

4. Docker/MySQL 环境(阶段 C)

  • Docker Engine 29.6.2;容器由负责人环境启动(本次仅 docker start 已存在容器,未重建、未删 volume)。
  • MySQL 8.4.11(127.0.0.1:3307,库 mydreamjob);Redis PONG;RabbitMQ healthy。
  • Flyway V1–V42 全部 success=1。

5. V43 真实迁移验收(阶段 D)

  • 迁移前三个种子账号脱敏快照:role=ADMIN/STUDENT/COMPANY,均 is_active=1,指纹与 V1_1 原始值完全匹配,user_roles 各 1 条,关联业务数据 4(applications)/21(jobs)/2(profiles)/1(company)。
  • 最新 JAR + dev(ddl-auto=validate)启动后 flyway_schema_history:V43 disable legacy seed accounts success=1,无失败迁移,V1–V42 校验和未变。
  • 三个种子账号 is_active 1→0;users 行仍存在;user_roles 仍存在;关联业务数据未删除。
  • 指纹偏差隔离夹具(7976/7977,改过手机号与哈希)在 V43 后仍 is_active=1,未误伤。
  • 原种子账号(demo 口令)登录返回 400 且无法取得 Token(已禁用账号不能建立会话),达成"不能取得 Token"目标。

6. 管理员初始化配置矩阵(阶段 E,真实 MySQL)

用例 结果 实测证据
E1 默认关闭 PASS 应用正常启动且未创建任何 ADMIN
E2 enabled=true+非法手机号 PASS 启动失败,固定文案 admin bootstrap rejected: configured phone is invalid
E3 无活动ADMIN+空闲手机号 PASS users 恰 1 行(role=ADMIN、active、verified、password_hash=NULL);user_roles 仅 ADMIN;无 Token/短信/通知;日志无手机号
E4 重复启动同配置 PASS skipped;users 与 ADMIN 角色计数均不增
E5 手机号被 STUDENT 占用 PASS 启动失败固定文案 phone is already in use;该 STUDENT 未被提权/复活/覆盖
E6 真实双 JAR 并发同手机号 PASS 两个独立进程 + 屏障同时初始化:最终 users 恰 1 行、ADMIN 角色恰 1 条;一方创建、一方安全幂等跳过;无手机号泄漏;无 SqlExceptionHelper 唯一键日志

前置与后置:临时禁用 20 个历史验收 ADMIN(id 逐一记录)并以精确 UPDATE 恢复原状;E3/E6 新建的验收 ADMIN 置为 inactive,未删除。

7. 启动最新 JAR 与双端口(阶段 F)

  • Started MyDreamjobApplication;无 Flyway/schema-validation 错误。
  • 8080 与 8081 均监听;8081 仅绑定 127.0.0.1。
  • GET /api/v1/health → 200/UP(仅最小状态,无 db/redis/rabbit/连接池字段)。
  • 运行时 OpenAPI:paths=176,operations=214,与静态一致。

8. 企业简历真实 HTTP 权限矩阵(阶段 G / H)

夹具:3 家企业(认证/未认证/其他)、6 名学生(APPLIED_COMPANIES·SUBMITTED、PRIVATE、无隐私记录、VERIFIED_COMPANIES·未认证企业、APPLIED_COMPANIES·WITHDRAWN、VERIFIED_COMPANIES·已认证企业),真实简历文件进存储基目录,投递/隐私由服务端推导。

用例 HTTP 期望 结果
匿名 401 401 PASS
STUDENT Token 403/2002 403 PASS
ADMIN Token 403/2002 403 PASS
其他企业 403/2002 403 PASS
未认证企业(岗位不属于) 403/2002 403 PASS
认证企业+APPLIED+SUBMITTED 200 200 PASS(下文字节/头断言)
PRIVATE 隐私 403/2002 403 PASS
无隐私记录(默认 PRIVATE) 403/2002 403 PASS
VERIFIED_COMPANIES+未认证企业 403/2002 403 PASS
APPLIED_COMPANIES+WITHDRAWN 403/2002 403 PASS
VERIFIED_COMPANIES+已认证企业 200 200 PASS(字节一致)
投递不存在 404/8002 404 PASS

成功下载断言:文件字节与夹具逐字节一致(RESUME-FIXTURE-A/F-20260915,字节级比较 PASS);Content-Disposition: attachment; filename="=?UTF-8?Q?...?="(净化文件名);Cache-Control: no-store;Content-Type 走白名单;响应不含存储路径/resumeUrl/studentUserId/手机号/邮箱。 日志/审计扫描:未发现简历正文标记、本地路径、S03 测试手机号、密钥等禁止字段。

9. Actuator 双端口真实验收(阶段 I)

管理端口 127.0.0.1:8081 — 全部 PASS

  • /actuator/health 200,JSON 仅含 status(最小化,无 components/db/redis/rabbit/diskSpace/hikari)。
  • /actuator/metrics 200(含 names 数组);/actuator/prometheus 200(Prometheus 文本、含 jvm_memory_used_bytes 等稳定指标)。
  • /actuator/env、/beans、/configprops、/heapdump、/threaddump 均不可读(401)。
  • 端口仅绑定 127.0.0.1;prometheus 文本未发现密码/Token/Bearer 等敏感值。

业务端口 8080 — FAIL(本轮关键缺陷)

路径 匿名 带 STUDENT Token(有效)
/actuator 401 500
/actuator/health 500 500
/actuator/metrics 500 500
/actuator/info 500 500
/actuator/prometheus 500 500
/actuator/env 401 500
/actuator/beans 401 500

任务书要求这些路径无论匿名或带 Token 一律 404;实测为 401/500。根因:SecurityConfig 对 /actuator/health/** 等显式 permitAll,但业务端口并不注册 Actuator 端点,请求落入 MVC 静态资源解析抛 NoResourceFoundException,被 GlobalExceptionHandler 通用 handler 映射为 500;未放行的 /actuator、/actuator/env、/actuator/beans 在匿名时被安全层拦为 401,带 Token 后同样 500。 同时,ActuatorIsolationIntegrationTest.businessPortDoesNotExposeActuatorEndpoints 断言这些路径为 404 且在 H2 测试环境下通过——测试环境(H2、无独立管理端口随机化 etc.)与真实 MySQL + 完整 Security 配置的生产行为不一致,测试未能捕获 401/500 差异,属测试真实性缺口。

10. 副作用(阶段 J)

下载成功、下载拒绝、下载 404 三类请求前后,对 15 张表(users、user_roles、student_profiles、resume_versions、applications、jobs、companies、auth_tokens、audit_logs、candidate_dimension_scores、notifications、messages、scoring_risk_events、scoring_risk_actions、talent_invitations)作行级全量快照对比:完全一致,无任何变化。PASS。

11. RabbitMQ(阶段 K)

4 个队列真实存在且 0 积压:queue.sms.verification、queue.notification、queue.audit、queue.notification.dlq(messages=0、ready=0、unacked=0)。管理员初始化与简历下载均未产生新增消息。PASS。

12. OpenAPI(阶段 L)

静态 verify_openapi.py:176 paths / 214 operations,全部通过(含 S03 简历下载契约、S02/S03 校验)。运行时 /api/v1/v3/api-docs:176/214 与静态一致。新端点仅 GET /company/candidates/{applicationId}/resume,无 requestBody、返回文件流;企业 DTO 不含 resumeUrl/storagePath/studentUserId/phone/email。PASS。

13. PASS / FAIL / SKIP 清单

  • PASS:阶段 A(A1–A7);阶段 B(1834 测试、OpenAPI 静态);阶段 C;阶段 D(V43 真实迁移与种子禁用、不误伤、登录不可用);阶段 E(E1–E6);阶段 F;阶段 G/H(12/12、字节与头断言、日志脱敏);阶段 I-2 管理端口;阶段 J;阶段 K;阶段 L。
  • FAIL:阶段 I-1(业务端口 Actuator 路径 401/500 而非 404)+ 关联测试真实性缺口(ActuatorIsolationIntegrationTest 与生产行为不一致)。
  • SKIP:无(核心项全部执行;仅"更大规模容量压测"不在本轮范围,不构成阻断)。

14. 剩余风险

  • 业务端口对 /actuator/health 等返回 500:属于错误响应泄漏(对探测者暴露服务器异常迹象),且不满足契约的 404 语义;修复后需补"业务端口带 Token 亦为 404"的真实 HTTP 覆盖。
  • 登录接口对已禁用账号返回 400 而非 403/401(无法取得 Token 已满足;状态码语义差异建议与契约对齐)。
  • 管理端口依赖网络隔离运维边界,生产中需确认部署层网络策略确实限制 8081 仅内网可访问。

15. 是否允许冻结 S03

不允许。 建议整改指令(下一轮定向修复,不修改本报告已执行的验收环境):

  1. 业务端口对未注册 Actuator 路径统一返回 404:
    • 在 GlobalExceptionHandler 增加 NoResourceFoundException 的精确映射(404),避免落入通用 500;或
    • 移除 SecurityConfig 中业务端口对 /actuator/** 的 permitAll 并显式声明业务端口 Actuator 不可达(任选其一,保持管理端口行为不变)。
  2. 增加真实 MySQL 环境下的 HTTP 断言:业务端口 /actuator、/actuator/health、/actuator/metrics、/actuator/prometheus、/actuator/env、/actuator/beans 在匿名与有效 STUDENT/COMPANY/ADMIN Token 下均为 404;并说明测试环境中为何出现 H2 404 而真实 MySQL 401/500 的差异,消除测试假绿。
  3. 修复后在最新 JAR + 真实 MySQL 上重跑阶段 I(业务端口 404 矩阵 + 管理端口只读矩阵)与 1834 项全量回归。

本报告不包含数据库密码、JWT、验证码、完整手机号、邮箱、验证手机号或简历正文。验收脚本与日志保留在 backend-infra/s03-*.ps1|.py|.log,夹具数据按"本轮唯一前缀/精确主键"清理并记录;audit_logs 未做删除。

16. Actuator 404 增量复验结果(定向整改后)

上一节记录了初次独立验收的唯一阻断:业务端口 8080 上 /actuator/** 匿名/带 Token 行为为 401/500 而非任务书要求的 404,且 H2 测试与生产行为不一致。针对该阻断完成定向整改后,使用最新 mvn clean verify JAR 与真实 MySQL 8.4.11 复验:

  1. 整改方式 —— SecurityConfig:原对 /actuator/health/**、/actuator/info、/actuator/metrics/**、/actuator/prometheus 的四个 permitAll 收敛为单一 .requestMatchers("/actuator/**").permitAll()。目的非暴露 Actuator(Actuator 仍只注册在独立管理端口),而是让所有 /actuator/** 请求进入 MVC 的不存在资源处理,消除匿名请求先被认证层转 401 的分支;业务端口未新增任何 Actuator Controller 或端点映射。
  2. 整改方式 —— GlobalExceptionHandler:新增 @ExceptionHandler(NoResourceFoundException.class),固定返回 HTTP 404、code=404、message=not found、requestId;不回显异常 message、请求路径、本地路径或堆栈;日志仅固定文案 + 归一化 requestId;未新增 ErrorCode。
  3. 业务端口矩阵(真实 HTTP):14 条路径(/actuator、/actuator/、/actuator/health、/actuator/health/liveness、/actuator/info、/actuator/metrics、/actuator/metrics/jvm.memory.used、/actuator/prometheus、/actuator/env、/actuator/beans、/actuator/configprops、/actuator/heapdump、/actuator/threaddump、/actuator/unknown)× 6 种身份(匿名 / STUDENT / COMPANY / ADMIN / 非法 Token / Refresh Token)共 84 个组合全部返回 404,且响应递归扫描无任何异常类名、框架包名、路径、Token、手机号等泄漏。
  4. 管理端口 127.0.0.1:8081:health(200,最小 JSON,仅 status)/ info(200)/ metrics(200,含 names)/ prometheus(200,含稳定 JVM 指标)白名单端点正常;env / beans / configprops / heapdump / threaddump / unknown 全部 404(未暴露端点未被修复带成 200);8081 仅绑定 127.0.0.1。
  5. 副作用:复验期间日志无 NoResourceFoundException 堆栈、无 ERROR;RabbitMQ 4 个队列(sms.verification / notification / audit / notification.dlq)0 积压;数据库无任何改动。
  6. 自动化:定向(Actuator/Security/GlobalExceptionHandler)61/61;全量 mvn clean verify 1836/1836(含新增 2 个 Actuator 复验测试),Failures=0、Errors=0、Skipped=0;OpenAPI 静态与运行时均为 176 paths / 214 operations,Actuator 不进入业务 OpenAPI。
  7. 一致性:V1–V42 未修改,V43 仍为唯一新增迁移;未新增 ErrorCode、公网业务接口、表或索引;应用进程已停止,当前未提交、未推送。

17. 最终结论

  • 初次独立验收发现业务端口 Actuator 401/500 阻断;
  • 完成定向整改(SecurityConfig 统一 /actuator/** 语义 + GlobalExceptionHandler 安全 404 映射 + 失败测试补齐);
  • 最新 JAR + 真实 MySQL/Docker 增量复验全部通过。

S03 独立代码复验和真实 MySQL/Docker 黑盒全部通过,可以进入精确暂存检查点。