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.
 
 
 

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 处)
  • 存量风险提示:DeletionGuardInterceptor:51 使用 chars().allMatch(isDigit) 预检,可挡 anonymousUser,但对 20 位纯数字溢出仍会抛 NumberFormatException(需合法签名 Token 才能到达)→ 列为后续轮次加固项

A8 OpenAPI 与响应兼容 —— PASS

  • 路径/operation 数量:静态 173/211;运行时 173/211,一致
  • 唯一变更:docs/openapi-rc.json 2 行描述文本(移除"验证码会打印到日志"表述),无结构变更
  • 登录、发码、岗位请求/响应 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(完整链路):

  1. 发送 → HTTP 200,DB 生成 1 条记录(验证码值仅存于库内,报告不复制)
  2. 从 DB 取码 → POST /auth/login → HTTP 200,响应含 accessToken/refreshToken/expiresIn/nickname/phone/role/userId,响应内不含验证码明文
  3. 登录后该记录 used=1、attempts=0(一次性消费语义不变)
  4. 重放同一验证码 → HTTP 400(拒绝)
  5. 日志与审计仍不含验证码/完整手机号
  6. 长度/有效期/最大尝试次数/锁定时间语义未变(SmsVerificationSecurityTest 10/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 个 SmsCodeSender Bean
  • 复现: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. 剩余风险

  1. F1/F2 未修复前,开发/测试人员仍会按文档从日志找验证码 → 必然失败并误判"系统故障"
  2. 存量 43 处直接身份解析(均不可匿名到达,但缺乏统一 CurrentUserAccessor 收口);DeletionGuardInterceptor 对 20 位数字溢出仍可能 500
  3. 验证码仍以明文存储于 sms_verification_codes.code(本轮未改表结构,属已知遗留)
  4. 真实短信供应商未接入;生产 mock=false 下所有发码请求均安全失败(设计如此,但需产品确认上线策略)
  5. 安全层 401 响应不含 requestId(既有 AuthenticationEntryPoint 固定载荷),与业务层错误响应格式不一致,不利于链路追踪
  6. 本轮验收脚本依赖 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 环境,日志中已无该行)
  • 实际结果:返回空字符串,登录步骤失败
  • 期望结果:改为受控来源取码,二选一——
    1. 数据库内部查询(推荐,仅限本地/测试): SELECT code FROM sms_verification_codes WHERE phone=? AND purpose='login' AND used=0 ORDER BY id DESC LIMIT 1;
    2. 或由测试夹具直接注入固定验证码记录后读取
  • 必须新增的失败测试:新增独立脚本断言 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 内新增,不改动既有断言):
    • emptyPropertyStillProvidesUnavailableSender
    • blankPropertyStillProvidesUnavailableSender
    • exactlyOneSenderBeanForEveryValueIncludingEmpty
  • 禁止的绕过:不得让 Mock 成为 matchIfMissing 默认;不得用 @Primary 掩盖歧义;不得让应用以空 Bean 启动(必须真实拒绝发码,不得伪造成功)

R4 —— 非阻断建议(可与上轮合并,不单独阻断冻结)

  1. DeletionGuardInterceptor:60 增加溢出安全解析(复用 CurrentUserAccessor 语义)
  2. 评估失败时释放 sms:rate:<phone> 限流键(F4)
  3. 排除 UserDetailsServiceAutoConfiguration 或显式声明 AuthenticationManager(F5)
  4. 统一为安全层 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不可冻结;验收报告已包含下一轮定向整改指令。