30 KiB
S01 认证安全上线阻断整改 · 独立代码复验与真实 MySQL/Docker 黑盒验收报告
- 验收日期:2026-09-11
- 验收性质:独立验收(非功能开发);使用真实 MySQL 8、最新 JAR、真实 HTTP 请求
- 验收结论:FAIL(已复现具体缺陷,S01 不可冻结)
- 报告文件:
docs/s01-independent-mysql-acceptance-20260911.md - 验收产物目录:
docs/acceptance/s01-20260911/(脚本、请求体、日志、结果快照)
本报告不包含数据库密码、.env 内容、JWT、验证码、完整手机号、明文测试口令与非法 principal 敏感标记原文。
1. Git 基线
| 项 | 实测值 |
|---|---|
| 分支 | fix/resume-upload-safety |
| HEAD | cdafbf8ac93544a3ef89d0f3014f822a0d575f8f(docs: update README for latest delivery baseline) |
| 最近 5 次提交 | cdafbf8 / 277b3c0 / dbd27e6 / cf39737 / 498512d |
| 暂存区 | 空(未执行 git add) |
git diff --check |
无空白错误(仅 CRLF 提示) |
| Flyway 迁移改动 | 无(V1–V41 原样,共 41 个迁移文件,最大版本 41) |
| 新增迁移 | 无 |
| 工作区修改 | 20 个(S01 8 个 + 简历上传安全 12 个,均未提交) |
| 未跟踪文件 | 基线 106 个,全部保留;本轮新增验收产物 |
| AGENTS.md | 未发现(仓库根、上级目录、.claude/ 均无 AGENTS.md/CLAUDE.md/GEMINI.md/.cursorrules) |
.env |
保留(365B) |
与 S01 无关的未提交修改(简历上传安全)已完整保护:profile/ProfileController.java、profile/ProfileService.java、profile/storage/LocalResumeStorageService.java、resume/ResumeVersionController.java、resume/ResumeVersionService.java、docs/frontend-integration.md、docs/resume-api.md 及 5 个测试文件——本轮未读取改写、未覆盖,时间戳仍为 2026-09-10。
2. 独立代码审查结果(阶段 A)
A1 验证码/手机号日志 —— PASS
| 检查项 | 结果 | 证据 |
|---|---|---|
| INFO/WARN/ERROR 不输出验证码 | ✅ | SmsVerificationService 仅 2 处 log.warn,参数为 maskPhone(phone);MockSmsCodeSender 仅 1 条无参固定文本 |
| 不输出完整手机号 | ✅ | 实测 7 个验收手机号在全部日志中命中 0 次 |
| Mock 实现仅固定安全文本 | ✅ | log.info("SMS verification code generated (mock sender)") |
| Service 不把验证码传入日志参数 | ✅ | 全量检索无 log.*code 参数传递 |
| AuthService 不输出完整手机号 | ✅ | 仅 log.info("Auto-created user: id={}") |
| JWT 过滤器不记录 Token/subject/异常原文 | ✅ | 3 条固定文本,无 e.getMessage()、无 subject |
| 审计 details 不含完整手机号/验证码 | ✅ | 实测唯一 SMS_SEND 明细为 phone=199****9999(脱敏) |
| 异常消息不含敏感值 | ✅ | SERVICE_UNAVAILABLE 固定文本 sms sender unavailable |
| 不通过 toString/getMessage 间接泄露 | ✅ | 捕获的异常与堆栈均不含非法 principal 标记 |
唯一敏感标记复验(真实 Logback 捕获 + 真实运行日志双通道):标记手机号、真实验证码(3 个 DB 实际值)、JWT 前缀 eyJ、Bearer ey、mock 正文、非法 principal 标记、NumberFormatException —— 命中数全部为 0。
A2 短信发送器装配 —— PASS(3 种配置)+ 发现 1 处边界缺陷(见 F3)
| 检查项 | 结果 |
|---|---|
Service 只依赖 SmsCodeSender 接口 |
✅ 单构造参数注入,无 if 分支复制双流程 |
true → 仅 MockSmsCodeSender |
✅ 实测(SmsSenderConfigurationTest 4/4 + 运行期行为) |
false → 仅 UnavailableSmsCodeSender |
✅ 实测 |
缺失 → 默认 UnavailableSmsCodeSender |
✅ 实测(D3a) |
| 单一 Bean / 无装配歧义 | ✅ 三次启动均成功(缺/多 Bean 会直接启动失败) |
| mock=false 不回退 Mock | ✅ 实测无 mock 事件 |
| 生产不意外启用 Mock | ✅ application.yml 默认 SMS_MOCK_ENABLED:false |
@ConditionalOnProperty 精确性 |
✅ Mock=havingValue="true"(无 matchIfMissing);Unavailable=havingValue="false", matchIfMissing=true;name 与配置项 app.sms.mock.enabled 完全一致 |
A3 mock=false 失败事务 —— PASS(MySQL 回滚)+ 1 项非阻断观察(F4)
@Transactional位于sendCode;顺序:校验 → 锁定/限流 → 生成 →codeRepository.save→ Redis 限流 →smsCodeSender.sendVerificationCode→ 审计BusinessException extends RuntimeException→ 默认回滚;发送异常未被捕获,不转换为空成功- 真实 MySQL 实测:失败后
sms_verification_codes完整字段前后完全一致(见第 6 节) - 观察项:限流键在发送之前写入,失败后残留 60s(既有排序,非 S01 引入)→ F4
A4 CurrentUserAccessor —— PASS
拒绝:null / !isAuthenticated / AnonymousAuthenticationToken / 空或空白 name / anonymousUser / 非 ASCII 纯数字(含 +42、" 42"、"42 "、Unicode 数字)/ Long 溢出 / userId<=0。
固定抛 InsufficientAuthenticationException(既有 @ExceptionHandler(AuthenticationException) → HTTP 401),不回显、不记录非法 principal,不新增 ErrorCode,不承担角色/归属判断,身份仅来自 SecurityContext。
A5 JwtAuthenticationFilter —— PASS
合法数字 subject 正常认证;缺失 Authorization → 匿名;无效签名/过期 → 既有 401;非数字 subject → 401 而非 500;日志无 Token/subject/异常原文;JwtTokenProvider 未改动(Token 兼容语义不变);未声称修复 Refresh Token。
A6 /jobs 安全规则 —— PASS
SecurityConfig:119 由 .requestMatchers("/jobs").permitAll() 改为 .requestMatchers(HttpMethod.GET, "/jobs").permitAll(),且该规则先于 .requestMatchers("/jobs/**").authenticated() 求值,写请求确实命中 authenticated。
全部 /jobs 映射与最终权限(21 个 operation,真实 HTTP 实测)
| 方法 | 路径 | 匿名 | STUDENT | COMPANY | ADMIN | 归属要求 |
|---|---|---|---|---|---|---|
| GET | /jobs |
200 | 200 | 200 | 200 | 无 |
| GET | /jobs/search、/jobs/filters |
200 | 200 | 200 | 200 | 无 |
| GET | /jobs/public/{id} |
200 | 200 | 200 | 200 | 无 |
| GET | /jobs/{id}/reviews、/summary、/{reviewId}、/jobs/compare |
200 | 200 | 200 | 200 | 无 |
| POST | /jobs |
401 | 403/2002 | 200 | 403/2002 | 需绑企业 |
| PUT | /jobs/{id} |
401 | 403/2002 | 200 | 403/2002 | 本企业(他企业 400/7002) |
| PATCH | /jobs/{id}/status |
401 | 403/2002 | 200 | 403/2002 | 本企业(他企业 400/7002) |
| DELETE | /jobs/{id} |
401 | — | — | — | 无该端点 |
| GET | /jobs/{id} |
401 | 403/2002 | 200 | 403/2002 | 本企业 |
| GET | /jobs/{id}/match |
401 | — | — | — | 登录 |
| GET | /jobs/{id}/competition-ranking |
401 | — | — | — | 登录 |
| POST/DELETE | /jobs/{id}/favorite、GET /jobs/favorites |
401 | — | — | — | 登录 |
| GET | /jobs/{id}/personalized-review-score |
401 | — | — | — | 登录 |
不依赖 Controller 解析 anonymousUser 失败来保护写接口:匿名请求在安全层即被拒(响应无 requestId 字段,符合既有 AuthenticationEntryPoint 固定载荷)。
A7 剩余直接身份解析 —— PASS(无 permitAll 可达)= 但存量 43 处
- 全量检索(任意变量名):
Long.(valueOf|parseLong)(*.getName())共 43 处,分布于 43 个文件 .isAuthenticated()共 44 处;anonymousUser字面量 5 处(PersonalDataExportController、DeletionGuardInterceptor×2、AdminReviewReplyController、CompanyReviewManagementController)- 逐端点核验 permitAll 可达性(按 SecurityConfig 匹配顺序,而非"路径要求认证"默认安全):
- permitAll 路径共 19 条规则,涉及的控制器为
AuthController、CompanyController、JobReviewController、JobController、MatchController、JobRecommendationController - 匿名可达的 8 个 GET 方法(
listJobs、searchJobs、getFilters、getPublicJob、list、get、summary、compare)均不调用身份解析 CompanyController的resolveUserId仅被/companies/me(POST/GET/PUT)使用 → authenticated;JobReviewController的resolveUserId仅被 create/update/delete 使用 → authenticated- 结论:43 处中 0 处处于 permitAll 可达路径,不构成 S01 发布阻断。
JobController已改用CurrentUserAccessor(裸解析 0 处)
- permitAll 路径共 19 条规则,涉及的控制器为
- 存量风险提示:
DeletionGuardInterceptor:51使用chars().allMatch(isDigit)预检,可挡anonymousUser,但对 20 位纯数字溢出仍会抛NumberFormatException(需合法签名 Token 才能到达)→ 列为后续轮次加固项
A8 OpenAPI 与响应兼容 —— PASS
- 路径/operation 数量:静态 173/211;运行时 173/211,一致
- 唯一变更:
docs/openapi-rc.json2 行描述文本(移除"验证码会打印到日志"表述),无结构变更 - 登录、发码、岗位请求/响应 Schema 未变(S01 未改任何 DTO)
- 无新增短信调试接口、无新增获取验证码接口
3. 自动化独立复验(阶段 B,实测重新执行)
| 命令 | Tests run | Failures | Errors | Skipped | 结果 |
|---|---|---|---|---|---|
-Dtest="SmsVerificationSecurityTest,SmsSenderConfigurationTest,CurrentUserAccessorTest,AuthenticationBoundaryIntegrationTest" |
40 | 0 | 0 | 0 | BUILD SUCCESS |
-Dtest="*Auth*Test,*Security*Test,*Sms*Test,*Job*Test" |
214 | 0 | 0 | 0 | BUILD SUCCESS |
mvn test |
1658 | 0 | 0 | 0 | BUILD SUCCESS(3:45) |
mvn clean verify |
1658 | 0 | 0 | 0 | BUILD SUCCESS(4:31,产出 JAR 79,965,063B) |
python ../docs/verify_openapi.py |
— | — | — | — | paths 173 / operations 211 / All checks passed! |
4. Docker / MySQL 环境(阶段 C)
启动前发现:docker ps 为空,三个业务容器处于 Exited(约 2 小时前,139/0/137 退出码),volume 数据完好。容器标签显示由 Desktop\qinghe\backend-infra\compose.yaml 创建(项目名 backend-infra)。
本轮处置(非破坏性,已记录):仅执行 docker start mydreamjob-mysql mydreamjob-redis mydreamjob-rabbitmq——未重建容器、未删除 volume、未清空数据库、未触碰其余 3 个无关孤立容器、未停止 Docker。
| 项 | 实测值 |
|---|---|
| Docker Server | 29.6.2 |
| MySQL | 8.4.11(127.0.0.1:3307,healthy) |
| Redis | 7.4-alpine(6379,healthy) |
| RabbitMQ | 3.13-management-alpine(5672/15672,healthy) |
| 验收数据库 | mydreamjob |
| Flyway | 41 条,success=1 全部成功,0 失败,最大版本 41(41 message recall) |
| S01 新增迁移 | 无 |
已修复的验收环境问题(脚本层,非项目缺陷):Git Bash(MSYS) 会把裸 / 的 RABBITMQ_VHOST 自动改写为 Git 安装根路径,导致 530 NOT_ALLOWED vhost not found。经在验收脚本中清除该变量(交由 application.yml 默认值 / 承接)后,RabbitMQ 错误数 0。
5. 三种配置启动验证(阶段 D,均为最新 mvn clean verify JAR)
| 场景 | 启动 | SmsCodeSender 实际实现 | 发码行为 | 日志敏感值 | 判定 |
|---|---|---|---|---|---|
D1 mock=true |
✅ Started 26.5s,health 200,8080 监听 | MockSmsCodeSender(固定事件文本命中 1 次) | HTTP 200(非 500) | 0 | PASS |
D2 mock=false |
✅ Started 26.4s | UnavailableSmsCodeSender(无 mock 事件) | HTTP 400 {"code":400,"message":"sms sender unavailable"} |
0 | PASS |
| D3a 属性完全缺失 | ✅ Started 27.5s | UnavailableSmsCodeSender(未回退 Mock) | HTTP 400 同上 | 0 | PASS |
| D3b 属性存在但为空值 | ❌ APPLICATION FAILED TO START | 无 Bean | — | — | FAIL(F3) |
每次启动均记录 profile(无 profile,基础 application.yml)、PID、端口、实现、Started 日志、health 状态;仅停止本轮启动并记录 PID 的 JAR(每次均以 Get-CimInstance Win32_Process 校验命令行确为 zhilai/mydreamjob-server/target/*.jar)。
6. 真实 MySQL 验证码事务回滚(阶段 E)
mock=false(发送失败回滚)——完整字段快照对比,非 COUNT(*):
| 指标 | BEFORE | AFTER | 结论 |
|---|---|---|---|
| 该手机号记录数 | 0 | 0 | ✅ |
| 全局 MAX(id) | 990 | 990 | ✅ 无新增 |
| 全局总行数 | 917 | 917 | ✅ |
| 未使用(used=0)记录数 | 0 | 0 | ✅ 无可用残留 |
| SMS_SEND 审计总数 | 819 | 819 | ✅ 无虚假成功审计 |
| 完整字段差异 | — | — | ✅ 前后完全一致 |
无半提交、无 attempts 异常、无错误锁定状态更新。HTTP 400(非虚假成功)。
mock=true(完整链路):
- 发送 → HTTP 200,DB 生成 1 条记录(验证码值仅存于库内,报告不复制)
- 从 DB 取码 →
POST /auth/login→ HTTP 200,响应含accessToken/refreshToken/expiresIn/nickname/phone/role/userId,响应内不含验证码明文 - 登录后该记录
used=1、attempts=0(一次性消费语义不变) - 重放同一验证码 → HTTP 400(拒绝)
- 日志与审计仍不含验证码/完整手机号
- 长度/有效期/最大尝试次数/锁定时间语义未变(
SmsVerificationSecurityTest10/10 覆盖)
7. 真实 HTTP 权限矩阵(阶段 F)
见第 2 节 A6 表格。关键断言全部成立:
- 匿名 POST /jobs = HTTP 401(
code=401),不是 500 ✅ - STUDENT / ADMIN POST /jobs = 403 / 2002 ✅
- COMPANY POST /jobs = 200 ✅(生成 id=43)
- 他企业 PUT/PATCH = 400 / 7002(既有语义,未擅改)✅
- 公开 GET(
/jobs、/search、/filters、/public/{id}、/{id}/reviews)语义不变 = 200 ✅ - 每条请求均记录 method / path / HTTP / business code / requestId
jobs 表副作用(完整字段对比,非 COUNT):
差异仅:新增 JOB:43|company_id=22|S01_ACC_*_job|PUBLISHED|...|requirements=S01_ACC_ok
TOTAL_JOBS 27 → 28
TAMPERED_TITLE=0 ← 他企业篡改尝试零落库
其余全部 JOB 行逐字段未变
8. 非法 Token / principal 黑盒(阶段 G)
对受保护端点 POST /jobs 的真实 HTTP 请求(含合法请求体):
| 用例 | HTTP | business code | 响应回显敏感标记 | 判定 |
|---|---|---|---|---|
| 无 Authorization | 401 | 401 | no | ✅ |
| 合法数字 subject(学生 7936) | 403 | 2002 | no | ✅ 正常进入既有权限判断 |
subject=anonymousUser |
401 | 401 | no | ✅ |
| 受控非法 principal 标记 | 401 | 401 | no | ✅ 非 500 |
| subject 缺失(空) | 401 | 401 | no | ✅ |
subject=0 |
401 | 401 | no | ✅ |
subject=-1 |
401 | 401 | no | ✅ |
| subject 超出 Long 范围 | 401 | 401 | no | ✅ |
| 过期 Token | 401 | 401 | no | ✅ |
| 签名被篡改 | 401 | 401 | no | ✅ |
无任何用例返回 500;响应与日志均未回显 subject 或 Token;jobs 表完整内容零变化(无业务写入) ✅
Token 生成方式:验收专用 HS256 离线生成(复用项目 JWT_SECRET 环境变量,未落盘、未输出);外部 HTTP 无需伪造即可安全覆盖全部用例,故未降级为纯集成测试。
9. 日志与审计全量脱敏(阶段 H)
仅扫描本轮日志(D1/D1b/D2/D3a/D3b 的 app + stdout):
| 禁止项 | 命中 | 说明 |
|---|---|---|
| 验证码明文(3 个 DB 实际值) | 0 | |
| 完整手机号(7 个验收号) | 0 | |
JWT(eyJhbGci、Bearer ey) |
0 | |
| Authorization 头值 | 0 | 16 处 authorization 均来自 D3b 启动失败堆栈的 AuthorizationFilter.doFilter |
| secret | 0 | |
| 非法 principal 标记 | 0 | |
NumberFormatException 堆栈 |
0 | |
| Mock 短信正文 | 0 | MOCK SMS CODE 命中 0 |
password |
⚠️ 16 | 全部为 Spring Boot 默认 Using generated security password: <uuid> → F5(既有行为,见第 12 节) |
审计表(audit_logs):完整手机号命中 0、非法 principal 命中 0、Token 命中 0、脱敏手机号命中 1(唯一 SMS_SEND 明细 phone=199****9999)✅
允许项:脱敏手机号、固定事件名、requestId、固定 purpose 均正常。
10. 运行时 OpenAPI(阶段 I)
| 项 | 值 |
|---|---|
| 访问地址 | GET /api/v1/v3/api-docs(应用 context-path=/api/v1;裸 /v3/api-docs 返回 404 属预期) |
| HTTP | 200 |
| 运行时 paths / operations | 173 / 211 |
| 静态 paths / operations | 173 / 211 |
| 一致性 | PASS |
| 短信相关路径 | 仅 /auth/sms/send(无新增调试/取码接口) |
/jobs 路径数 |
15 |
/auth/sms/send 描述 |
已不再声称验证码输出到日志/控制台 ✅ |
11. 文档一致性核查(阶段 13)—— FAIL
仍指导从日志读取验证码的文档(精确文件:行号):
| 文件 | 行号 | 内容(已脱敏) | 性质 |
|---|---|---|---|
docs/acceptance-week1.md |
94 | # Mock 登录(从日志获取验证码) |
历史验收记录 |
docs/acceptance-week1.md |
148 | 注: 验证码从应用日志中获取 | 历史验收记录 |
docs/acceptance-week1.md |
152 | 验证码打印到日志,不发送真实短信 | 历史验收记录 |
docs/api-v0.1.md |
27 | Mock 环境验证码打印到日志 | 历史 API 文档 |
docs/api-v0.1.md |
174 | 短信验证码为 Mock 实现,验证码打印到日志 | 历史 API 文档 |
docs/frontend-integration.md |
37,39,42,144 | "Mock 验证码日志位置"、"打印在服务控制台/stdout 日志中"、旧示例行 MOCK SMS CODE: phone=138****0002, … code=****** |
前端接入文档(在途修改文件,本轮不可改) |
docs/offer-api.md |
262 | 测试账号(Mock 验证码,见日志) | 活文档 |
docs/resume-api.md |
189 | 登录学生账号(Mock 验证码,见日志) | 在途修改文件 |
docs/smoke-test.sh |
64-71 | sms_code():grep -a "MOCK SMS CODE.*phone=$PHONE" $log | … | grep -oE 'code=[0-9]{6}' |
git 跟踪的既有可执行脚本(见 F1) |
README.md:167 表述正确(禁止写入敏感信息),不计入违规;docs/openapi-rc.json:1449 已修正 ✅。
12. RabbitMQ 与外围终态(阶段 14)
| 项 | 实测值 |
|---|---|
| 队列 | queue.sms.verification、queue.notification、queue.audit、queue.notification.dlq |
| ready / unacked | 全部 0 / 0 |
| mock=false 失败后 notifications 增量 | 0 |
| mock=false 失败后 SMS_SEND 审计增量 | 0(无虚假发送成功审计) |
| 匿名岗位写请求副作用 | jobs/notifications/audit 均无新增 |
| 无与 S01 无关的表变化 | ✅(仅本轮验收夹具,已精确清理) |
| 容器终态 | 3 容器仍 Up(healthy);未停止 Docker |
13. PASS / FAIL / SKIP 清单
| # | 验收项 | 结果 |
|---|---|---|
| A1 | 验证码/手机号不进入日志与审计 | PASS |
| A2 | SMS 发送器装配(true/false/缺失) | PASS |
| A2′ | 空值边界下的 Bean 唯一性 | FAIL(F3) |
| A3 | mock=false 失败事务回滚(真实 MySQL) | PASS(F4 为非阻断观察) |
| A4 | CurrentUserAccessor 边界 | PASS |
| A5 | JwtAuthenticationFilter 边界 | PASS |
| A6 | /jobs 匹配顺序与权限矩阵 | PASS |
| A7 | 无 permitAll 可达的直接身份解析 | PASS(存量 43 处均不可匿名到达) |
| A8 | OpenAPI 与响应兼容 | PASS |
| B | 自动化复验(40/214/1658/clean verify) | PASS |
| C | Docker/MySQL/Flyway | PASS |
| D | 三种配置启动 | PASS(D1/D2/D3a) |
| E | 真实 MySQL 回滚 + mock=true 链路 | PASS |
| F | 真实 HTTP 权限矩阵 | PASS |
| G | 非法 Token/principal 黑盒 | PASS |
| H | 日志与审计脱敏(password 项除外) |
PASS |
| I | 运行时 OpenAPI | PASS |
| 13 | 文档不再指导从日志读取验证码 | FAIL(F1、F2) |
| 14 | RabbitMQ 与外围绕 | PASS |
| — | SKIP | 0(核心验收项无 SKIP) |
FAIL 计数:3(F1、F2、F3)
14. 缺陷详情(可复现)
F1(阻断)项目自带冒烟脚本因 S01 改动而失效
- 文件/方法:
docs/smoke-test.sh→sms_code()(第 64-71 行,git 跟踪,提交b01616d) - 缺陷:该函数依赖被 S01 删除的日志行
MOCK SMS CODE: phone=…, code=…提取验证码 - 复现:
LOG=<本轮 mock=true 应用日志> grep -a "MOCK SMS CODE.*phone=<验收手机号>" "$LOG" | tail -n 1 | grep -a -oE 'code=[0-9]{6}' | cut -d= -f2 - 实际结果:输出为空 →
SMS=""→ 后续登录步骤拿不到验证码,冒烟流程中断 - 期望结果:脚本应从数据库/受控夹具取得验证码(与 S01 新语义一致)
- 影响:既有可执行验收工具功能性回归;S01 整改报告将其列为"文档遗留"但未识别此处为可执行依赖
F2(阻断,对应验收标准第 17 项)文档仍指导从日志读取验证码
- 文件/行号:见第 11 节表格(
acceptance-week1.md:94,148,152、api-v0.1.md:27,174、frontend-integration.md:37,39,42,144、offer-api.md:262、resume-api.md:189) - 实际结果:文档仍声称"验证码打印到日志/控制台"并指导据此取码
- 期望结果:明确"日志不再输出验证码";dev/test 通过受控测试夹具或数据库内部验收方式取码;禁止生产查询验证码表完成登录;说明
mock=false且无真实供应商时返回服务不可用;真实短信供应商待单独接入 - 约束:
docs/frontend-integration.md、docs/resume-api.md属在途修改文件,本轮不得改动
F3(非阻断但违反 A2 明确要求)空值配置导致启动失败
- 文件:
auth/sms/MockSmsCodeSender.java(@ConditionalOnProperty(name="app.sms.mock.enabled", havingValue="true"))与auth/sms/UnavailableSmsCodeSender.java(havingValue="false", matchIfMissing=true) - 缺陷:属性存在但为空值(如 env 中
SMS_MOCK_ENABLED=)时,两个条件都不匹配 → 0 个SmsCodeSenderBean - 复现:
java -jar target/mydreamjob-server-*.jar --app.sms.mock.enabled=(其余同 D2) - 实际结果:
APPLICATION FAILED TO START Parameter 3 of constructor in com.mydreamjob.auth.SmsVerificationService required a bean of type 'com.mydreamjob.auth.sms.SmsCodeSender' that could not be found. - 期望结果:任何取值(含空值/空白)都应至少装配一个实现,绝不因缺少 Bean 以模糊 Spring Bean 错误启动失败(原工作令第 11 条)
- 影响:部署可用性风险(空 env 变量在生产会致全站不可启动);未造成数据泄露
F4(观察,非阻断)失败后残留限流键
- 现象:mock=false 发送失败后
sms:rate:<phone>残留(TTL 60s),60s 内重试返回code=400 sms rate limited而非sms sender unavailable - 定性:既有排序(限流键在发送前写入),S01 未改变该顺序;MySQL 无残留有效验证码;不属冻结阻断项
- 建议:后续轮次评估失败时释放限流键或调整写入顺序
F5(观察,非阻断,既有行为)Spring Boot 默认安全口令入日志
- 现象:日志出现
Using generated security password: <uuid> - 定性:Spring Boot
UserDetailsServiceAutoConfiguration默认行为;经历史日志比对确认 S01 之前即存在(mydreamjob-server/logs/backend-startup.log等) - 可利用性:应用未启用 httpBasic/formLogin,该口令无 HTTP 认证入口消费 → 不可用于登录
- 建议:后续轮次排除
UserDetailsServiceAutoConfiguration或显式声明AuthenticationManager
15. 剩余风险
- F1/F2 未修复前,开发/测试人员仍会按文档从日志找验证码 → 必然失败并误判"系统故障"
- 存量 43 处直接身份解析(均不可匿名到达,但缺乏统一
CurrentUserAccessor收口);DeletionGuardInterceptor对 20 位数字溢出仍可能 500 - 验证码仍以明文存储于
sms_verification_codes.code(本轮未改表结构,属已知遗留) - 真实短信供应商未接入;生产
mock=false下所有发码请求均安全失败(设计如此,但需产品确认上线策略) - 安全层 401 响应不含
requestId(既有AuthenticationEntryPoint固定载荷),与业务层错误响应格式不一致,不利于链路追踪 - 本轮验收脚本依赖 Git Bash 环境变量语义(vhost 转换问题已规避并记录)
16. 是否允许冻结 S01
不允许冻结。
自动化指标(1658/1658、clean verify、静态与运行时 OpenAPI 173/211)全部达成,核心安全目标(日志脱敏、SMS_MOCK_ENABLED 生效、匿名 POST /jobs 401、非法 subject 401 不回显、真实 MySQL 回滚)均已独立复验通过;但存在 3 项 FAIL(F1 可执行脚本回归、F2 文档违规指导、F3 空值启动失败),其中 F2 直接命中验收标准第 17 项,F1 为可执行功能回归。
17. S01 下一轮定向整改指令
R1 —— 修复冒烟脚本取码方式(对应 F1)
- 文件:
docs/smoke-test.sh - 方法:
sms_code() - 错误数据/逻辑:
grep -a "MOCK SMS CODE.*phone=$PHONE" "$log_path" | tail -n 1 | grep -a -oE 'code=[0-9]{6}' | cut -d= -f2 - 复现请求:
bash docs/smoke-test.sh(mock=true 环境,日志中已无该行) - 实际结果:返回空字符串,登录步骤失败
- 期望结果:改为受控来源取码,二选一——
- 数据库内部查询(推荐,仅限本地/测试):
SELECT code FROM sms_verification_codes WHERE phone=? AND purpose='login' AND used=0 ORDER BY id DESC LIMIT 1; - 或由测试夹具直接注入固定验证码记录后读取
- 数据库内部查询(推荐,仅限本地/测试):
- 必须新增的失败测试:新增独立脚本断言
sms_code()在"日志不含MOCK SMS CODE"时仍能返回 6 位码(当前实现必然失败) - 禁止的绕过:不得恢复
MOCK SMS CODE日志行;不得让脚本读取生产日志取码
R2 —— 修正文档中的取码指引(对应 F2,命中验收标准第 17 项)
- 文件与行号:
docs/acceptance-week1.md:94,148,152;docs/api-v0.1.md:27,174;docs/offer-api.md:262;docs/frontend-integration.md:37,39,42,144;docs/resume-api.md:189 - 错误内容:声称"验证码打印到日志/控制台"并指导据此取码(含旧示例行)
- 复现:
rg -n "控制台|日志.*验证码|MOCK SMS|验证码.*日志|查看验证码" docs README.md - 实际结果:上述 10 处仍存在
- 期望结果:统一替换为——
- 日志不再输出验证码;
- dev/test 环境通过受控测试夹具或数据库内部验收方式取得验证码;
- 禁止生产环境查询验证码表完成登录;
mock=false且未接入真实供应商时返回"服务不可用",绝不返回虚假成功;- 真实短信供应商仍待单独接入。
- 协调要求:
docs/frontend-integration.md、docs/resume-api.md属简历上传安全在途修改文件,需与该项变更方合并处理,避免互相覆盖 - 必须新增的失败测试:新增文档门禁测试(或 CI 检查)断言
docs/**与README.md不再匹配MOCK SMS CODE|验证码.*打印.*日志|控制台.*验证码|见日志.*验证码 - 禁止的绕过:不得删除或改写为同义表述;不得仅在 README 声明而保留违规正文
R3 —— 空值配置不再导致启动失败(对应 F3)
- 文件:
auth/sms/MockSmsCodeSender.java、auth/sms/UnavailableSmsCodeSender.java(必要时新增无条件兜底实现) - 错误配置:
app.sms.mock.enabled=(存在但空) - 复现请求:
java -jar target/mydreamjob-server-0.0.1-SNAPSHOT.jar --app.sms.mock.enabled=(DB/Redis/RabbitMQ 同 D2) - 实际结果:
APPLICATION FAILED TO START … required a bean of type 'SmsCodeSender' that could not be found - 期望结果:启动成功并装配
UnavailableSmsCodeSender(空值、空白值、缺失、false四者行为一致;仅显式true才装配 Mock) - 必须新增的失败测试(
SmsSenderConfigurationTest内新增,不改动既有断言):emptyPropertyStillProvidesUnavailableSenderblankPropertyStillProvidesUnavailableSenderexactlyOneSenderBeanForEveryValueIncludingEmpty
- 禁止的绕过:不得让 Mock 成为
matchIfMissing默认;不得用@Primary掩盖歧义;不得让应用以空 Bean 启动(必须真实拒绝发码,不得伪造成功)
R4 —— 非阻断建议(可与上轮合并,不单独阻断冻结)
DeletionGuardInterceptor:60增加溢出安全解析(复用CurrentUserAccessor语义)- 评估失败时释放
sms:rate:<phone>限流键(F4) - 排除
UserDetailsServiceAutoConfiguration或显式声明AuthenticationManager(F5) - 统一为安全层 401 响应补充
requestId(便于链路追踪)
18. 验收产物清单(docs/acceptance/s01-20260911/)
| 文件 | 用途 |
|---|---|
launch.sh |
三种配置启动器(true/false/MISSING/EMPTY) |
gen-token.js |
验收专用 HS256 Token 离线生成(密钥仅取自环境变量,不落盘) |
matrix.sh / matrix-results.txt |
阶段 F 权限矩阵脚本与结果 |
phase-g.sh / phase-g-results.txt |
阶段 G 非法 Token 边界脚本与结果 |
snapshot-jobs.sql / jobs-before.txt / jobs-after.txt / jobs-before-g.txt / jobs-after-g.txt |
jobs 表完整字段快照与对比 |
snapshot-sms.sql / before-mockfalse.txt / after-mockfalse.txt |
阶段 E 验证码表回滚对比 |
job-body.json / tamper-body.json / status-body.json / ok-body.json |
请求体夹具(ASCII,避免 shell 编码问题) |
d1*/d2*/d3* 日志 |
三次配置启动的应用日志与 stdout |
runtime-openapi.json |
运行时 OpenAPI 快照 |
acc-fixture.txt / acc-prefix.txt |
验收夹具 ID 记录(已清理) |
sensitive-markers.txt |
敏感标记清单(内容未写入报告) |
验收数据清理:按精确 ID 与前缀删除 users(7933-7937)/companies(22,23)/jobs(42,43)/sms_verification_codes(1),Redis 验收键已清理;保留本轮窗口内 2 条 audit_logs(append-only 合规记录,不做删除);jobs 总数回到基线 26。清理前后均确认目标归属,无模糊 DELETE。
已复现具体缺陷,S01不可冻结;验收报告已包含下一轮定向整改指令。