# 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 黑盒全部通过,可以进入精确暂存检查点。**