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.
 
 
 

12 KiB

S02 独立代码复验与真实 MySQL/Docker 黑盒验收报告(脱敏版)

  • 验收日期:2026-09-14
  • 分支:feat/s02-token-isolation
  • 验收对象:S02「Access/Refresh Token 隔离、轮换与吊销」
  • 结论:PASS —— S02独立代码复验和真实MySQL/Docker黑盒全部通过,可以进入精确暂存检查点。
  • 本报告不含:JWT 原文、Token 摘要全文、验证码、完整手机号、数据库口令、JWT 密钥、.env 内容、Authorization 请求头

1. Git 基线与工作区状态

项 实测
分支 feat/s02-token-isolation
HEAD 96a920b fix: harden resume upload safety
暂存区 空
git diff --check 无空白错误
工作区 63 个已修改 + 116 个未跟踪
唯一新增迁移 V42__auth_token_sessions.sql(V1–V41 未修改)
删除文件 0
AGENTS.md 未发现
静态 OpenAPI 175 paths / 213 operations(All checks passed!)

2. 独立代码审查逐项结论

项 结论 关键证据
A1 Token 类型隔离 PASS Access 用 accessKey、带 type=access;Refresh 用 refreshKey、带 type=refresh、无 role;摘要用独立 tokenHashSecret;三项密钥经 requireDistinct 强制互异;两类 Token 均不含 phone/email/nickname/resumeUrl
A2 JWT claims PASS JwtTokenProvider.parse() 统一校验 type/iss/aud/exp/iat,requirePositiveSubject 与 requireCanonicalUuid 校验 sub/sid/jti;时间契约 iat <= capturedNow < exp;异常统一 InvalidTokenException(固定文本,不含 Token/claim)
A3 服务端会话状态 PASS JwtAuthenticationFilter 验签后调用 isSessionActive:用户存在 + active + session 下存在未消费/未撤销/未过期记录;session_id IS NULL 永不命中;异常失败关闭;createSession 对 inactive 用户先抛异常(不先发 Token)
A4 刷新轮换事务 PASS refresh() 无 @Transactional(编排入口);executeRefreshAttempt 在独立事务;消费与写后继同事务;条件更新含 id/version/revoked/consumedAt/expiredAt;consumed != 1 即判定重放;重放信息经 RefreshAttempt.Replayed 返回而非抛异常;重放撤销在尝试事务之后执行;无 JVM 锁、无 Thread.sleep
A5 并发重放 PASS revokeReplayedSession 用 REQUIRES_NEW,逐轮 revokeSession 后校验活动 Token 数,最多重试 3 次(无 sleep),仍失败则抛出(不静默)。日志中已无 could not take locks 分支
A6 logout PASS 只撤销当前 session;幂等;其他设备不受影响;测试未使用 catch (Throwable),未在最终断言前补 logout
A7 Token 摘要与脱敏 PASS HMAC-SHA256 + 独立密钥;64 位小写十六进制;MessageDigest.isEqual 常量时间比较;hash(null) 与 hash("") 同值但生产路径在校验后才调用
A8 capturedNow PASS 登录/刷新/登出每个入口只读一次 Clock;created_at/expired_at/consumed_at/revoked_at/审计均来自该次捕获;固定时区 Asia/Shanghai
A9 用户禁用与注销 PASS createSession 拒绝 inactive;注销完成在同一业务事务内 desensitize()(active=false)+ revokeAllUserSessions;回退状态不恢复已撤销 Token;其他用户不受影响
A10 配置安全 PASS(代码层) 三项密钥缺失/过短/相同/issuer 空/audience 空均启动失败,错误只含密钥名不含值;旧 JWT_SECRET 不再强制解析;CandidateKeyGenerator 在空密钥时先抛异常,不产生可预测 key

观察项(非阻断):AppJwtProperties 的校验规则没有专门的单元测试(设计 §六 曾要求)。本轮改用阶段 O 的真实 JAR 启动矩阵实证,覆盖更强;仍建议后续补一个配置校验单测。

3. Docker / MySQL / Redis / RabbitMQ

组件 版本 状态
Docker Engine 29.6.2(linux/amd64) 可用
mydreamjob-mysql MySQL 8.4.11 healthy(127.0.0.1:3307)
mydreamjob-redis redis:7.4-alpine healthy
mydreamjob-rabbitmq rabbitmq:3.13-management-alpine healthy

验收数据库名:mydreamjob。未重建容器、未删除 volume、未打印 .env 或口令。

4–6. Flyway / 表结构 / 启动

Flyway:接受验收前最新版本为 V41;启动最新 JAR 后 V42 auth token sessions success=1,失败迁移 0。

auth_tokens 真实表结构(新增 7 列 + 3 索引,旧索引保留):

session_id VARCHAR(36) NULL      token_jti VARCHAR(36) NULL
parent_token_id BIGINT NULL      consumed_at / revoked_at DATETIME NULL
revocation_reason VARCHAR(40)    version BIGINT NOT NULL DEFAULT 0
UNIQUE uk_auth_tokens_token_jti (token_jti)
KEY idx_auth_tokens_session_active (session_id, revoked, consumed_at, expired_at)
KEY idx_auth_tokens_user_session (user_id, session_id)
KEY idx_auth_tokens_user / idx_auth_tokens_hash (既有,保留)

无任何保存 Token 原文的列。

启动:Started MyDreamjobApplication(16.3 s),无 Flyway 错误、无 schema validation 错误;GET /api/v1/health → HTTP 200 / status=UP;运行时 OpenAPI 175/213。

7. 自动化实际测试数

命令 Tests run Failures Errors Skipped BUILD
原独立失败测试(2 类) 3 0 0 0 SUCCESS
S02 专项(13 类) 84 0 0 0 SUCCESS
认证关联回归 208 0 0 0 SUCCESS
mvn test 1758 0 0 0 SUCCESS
mvn clean verify 1758 0 0 0 SUCCESS(2:21)

(候选基线称"扩展专项 87/87" = 专项 84 + 独立 3,与实测一致。)

8–9. Token claims 与类型矩阵(真实 HTTP)

claims:登录响应字段兼容、expiresIn=86400、三段 JWT、两 Token 原文不同、jti 不同、sid 相同、type 正确、iss/aud 正确、Access 有 role、Refresh 无 role、均不含个人信息、有效期 24h/7d、均含 iat —— 全部 PASS。

类型矩阵(每格记录 method / path / HTTP status / business code / requestId):

Token /auth/me /auth/refresh
Access 200 401
Refresh 401 200(并轮换)
无 type 旧 Token 401 401
Access 用 Refresh 密钥伪造 401 401
Refresh 用 Access 密钥伪造 401 401
过期 Token 401 401
缺 iat 401 401
未来 iat 401 401
非法 sid 401 401
非法 sub 401 401

错误响应均未泄漏 JWT 原文、Bearer、token_hash、sid、jti、手机号、密钥、异常类名或堆栈。

10–11. 正常轮换与串行重放

轮换:sid 不变、jti 变化、旧记录 consumed_at 非空、version 只 +1、新记录 parent_token_id 指向旧记录、同一时刻仅 1 个活动 Refresh、A2 可访问、R2 可再次刷新 —— 全部 PASS。

串行重放:R1 重放 → 401;当前 session 活动 Refresh 数 0;A1 与 A2 访问 /auth/me 均 401;R2 刷新 401;其他设备 session 200;写入一次重放安全审计。

12–13. 真实并发(10 轮)与胜出方 Access

每轮两个独立 HTTP 客户端 + threading.Barrier 同时发起,10/10 轮全部满足:

断言 实测
无 HTTP 500 10/10
最多一个刷新成功 10/10
失败方为 401 10/10
重放后活动 Refresh 严格为 0 10/10
原 Access 与胜出方新 Access 均失效 20 次断言全部满足
其他设备 session 始终 200 10/10
无锁异常 / 堆栈泄漏 10/10
日志无 refresh reuse revocation could not take locks ✅
无同一 session 两个活动后继 ✅

14–16. 多设备 / 竞争 / inactive 与注销

  • logout:第一次成功,Access 与 Refresh 随即 401;重复 logout 按既有幂等语义;其他设备不受影响。
  • 多设备隔离:两设备 sessionId 不同;A 登出后 A 失效、B 的 Access 与 Refresh 均保持有效。
  • logout 与 refresh 并发:无 500;未补 logout 的情况下活动 Refresh 数为 0;竞争可能返回的新 Access 最终 401。
  • inactive:登录不返回 Token、不新增 auth_tokens;置为 inactive 后旧 Access/Refresh 均 401。
  • 注销完成(走正式懒转换完成路径):状态 COMPLETED、用户 active=false、全部会话撤销、两台设备 Access/Refresh 均 401、其他用户完全不受影响。

17. Clock 与真实数据库时间

断言 实测
created_at 与 Access iat 同一时刻 ±1s 内一致
expired_at 与 Refresh exp 同一时刻 ±1s 内一致
consumed_at 与刷新时刻 ±1s 内一致
revoked_at 与登出时刻 ±5s 内一致
AUTH 审计 created_at 与请求时刻 ±120s 内一致

18–19. 明文扫描 / 审计与日志脱敏

  • token_hash 为 64 位小写十六进制,不等于 Refresh 原文;库中搜索两类 Token 原文命中 0。
  • 应用日志与 stdout 扫描:Token / 验证码 / 密钥命中 0;扫描器经注入探针验证有效。
  • 审计动作仅限 4 个白名单值;details 不含 Token、摘要、Bearer、手机号、jti、Authorization、密钥;不含原始 sid(用不可逆 sessionRef)。

20–22. 副作用 / RabbitMQ / OpenAPI

  • 数据库副作用:登录+刷新+串行重放前后,candidate_dimension_scores、messages、notifications、applications、talent_invitations 内容摘要零变化(按 id 摘要比对,非仅 COUNT)。
  • RabbitMQ:4 个项目队列全部存在,ready=0、unacked=0;S02 运行期间无新增消息。
  • OpenAPI:静态与运行时均 175 / 213;/auth/refresh 请求体仅 refreshToken;/auth/logout 无请求体;公共 schema 不含 tokenHash/sessionId/phone/secret 等内部字段。

23. PASS / FAIL / SKIP 清单

阶段 PASS FAIL SKIP
A 代码审查(10 项) 10 0 0
B 自动化(5 组) 5 0 0
C–E 环境/迁移/启动 8 0 0
F–M 黑盒(含 10 轮并发) 117 0 0
N–S 时间/脱敏/副作用/外围/OpenAPI 30 0 0
O 配置启动矩阵(9 配置) 18 0 0
合计 165 0 0

24. 剩余风险

  1. 重放撤销重试可能重复写审计:revokeReplayedSession 在需要重试时会再写一对 AUTH_REFRESH_REUSE_DETECTED / AUTH_SESSION_REVOKED。本轮所有场景(含 10 轮并发)重试次数为 0,未观测到重复;属潜在项,建议后续把审计移到循环外。
  2. AppJwtProperties 无专门单元测试(见第 2 节观察项)。
  3. 旧 JWT_SECRET 仍被 talentdiscovery.CandidateKeyGenerator 使用:本轮已确认空密钥时安全失败(抛异常、不产生可预测 key),但该配置项仍存在;建议后续为其单独引入专用密钥。
  4. 本轮未执行:真实短信供应商、Redis 会话缓存、设备指纹、Access 有效期缩短等 S02 明确非目标项。
  5. 验收数据保留在库(见第 26 节)。

25. 是否允许冻结 S02

允许。 24 项验收判据全部满足,无 FAIL、无核心项 SKIP,24 条"不得冻结"清单全部未触发。


26. 验收结束处理

  • 本轮启动的 JAR(PID 21420,命令行含 s02-acc-main-app.log)已停止;8080 恢复空闲;未停止任何未知 Java 进程;未停止 Docker。
  • 验收数据保留,未执行删除。理由:audit_logs 有 143 条 AUTH_* 行引用本轮验收用户且该表无外键,删除用户会产生孤儿审计行;而本任务边界禁止删除 audit_logs。精确足迹如下(供后续人工处置):
范围 行数
users(本轮 15:25 之后创建,199 前缀) 35
auth_tokens(这些用户) 93
audit_logs AUTH_*(这些用户) 143
account_deletions(这些用户) 1

注:库中另有 3 个 199 前缀用户属于 S01 验收(2026-09-14 09:48 与 2026-08-13),及其 2 条投递记录,非本轮产生,未触碰。

  • 最终 Git:暂存区空、git diff --check 无错误、V1–V41 未修改、V42 为唯一新增迁移、未删除文件、未 git add -A / commit / push、未进入 S03。

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