23 KiB
R01-A6 独立代码复验报告(最终批:5 个 Controller 请求身份与 requestId 收束)
日期:2026-09-17
分支:refactor/r01-structure-performance
HEAD:534afe686b966f175bcaf4af0726cec061092050(A5 提交;复验全程未变)
暂存区:空
复验对象:docs/r01-a6-final-request-context-migration-report.md
性质:仅复验 —— 未修改生产代码、未修改既有测试断言、未暂存、未提交、未推送
0. 结论
R01-A6独立代码复验通过,可以进入精确暂存与独立提交检查点;A系列到此结束,不再设置A7/A8。FairnessMonitoringControllerTest的单跑夹具缺陷须在A6提交后以独立纯测试提交修复。
业务 Controller 身份解析残留 = 0;业务 Controller requestId 重复解析残留 = 0。 剩余命中全部落在基础设施职责内(身份 3 文件、requestId 5 文件)。
未发现 A6 引入的生产缺陷。 FairnessMonitoringControllerTest 的单跑失败经证据链确认为
测试夹具缺陷(TestAuthTokens 对"已存在但被禁用"的用户行不做激活),
与 A6 生产改动无关;其准确根因见 §9。
1. 开始前状态
| 项 | 结果 |
|---|---|
git branch --show-current |
refactor/r01-structure-performance |
git rev-parse HEAD |
534afe686b966f175bcaf4af0726cec061092050 |
git diff --cached --name-only |
空(暂存区为空) |
git diff --check |
仅 docs/backend-development-roadmap.md 的 LF/CRLF 警告,exit 0,无空白错误 |
| Java/Maven 残留进程 | 无 |
工作区改动共 6 个文件:5 个生产 Controller + docs/backend-development-roadmap.md。
2. 五个 Controller 静态审查
git diff --stat:
docs/backend-development-roadmap.md | 2 +-
.../com/mydreamjob/controller/ApiController.java | 12 +++++---
.../com/mydreamjob/controller/AuthController.java | 33 +++++++++++-----------
.../mydreamjob/controller/SystemController.java | 14 +++++----
.../java/com/mydreamjob/job/JobController.java | 21 +++++++-------
.../FairnessMonitoringController.java | 25 +++++++---------
6 files changed, 54 insertions(+), 53 deletions(-)
| 项 | 结果 |
|---|---|
| 修改范围是否仅限 A6 清单内 5 个 Controller | 是 ✔ |
| 是否新增接口 / ErrorCode / Flyway / 表 / 索引 / 依赖 | 否 ✔ |
是否触碰 SecurityConfig / Filter / Accessor / GlobalExceptionHandler |
否 ✔ |
| 是否修改既有测试文件 | 否 ✔(git status 中无任何 src/test 既有文件被修改) |
2.1 禁止模式扫描(静态门禁)
对 5 个 Controller 逐一扫描 X-Request-Id 直读、SecurityContextHolder、
getAuthentication()、Long.valueOf(auth.getName()):
| 文件 | 命中 |
|---|---|
controller/ApiController.java |
0 ✔ |
controller/AuthController.java |
0 ✔ |
controller/SystemController.java |
0 ✔ |
job/JobController.java |
0 ✔ |
scoringmonitoring/FairnessMonitoringController.java |
0 ✔ |
全仓 *Controller.java 扫描同一组模式:0 命中 ✔
2.2 构造器注入与无回退
ApiController、SystemController 新增 @RequiredArgsConstructor 并声明
private final CurrentUserAccessor / private final RequestIdAccessor;
AuthController、JobController、FairnessMonitoringController 复用既有
@RequiredArgsConstructor。全部为 final 构造器注入,无字段默认值、无 null 回退、
无 new 兜底实现 ✔
2.3 GlobalExceptionHandler.java 三方 blob 比对
git hash-object <file> → 1fdc341ebe3653393b3ec04f0c2d0bb9dfbb6d21
git rev-parse HEAD:<file> → 1fdc341ebe3653393b3ec04f0c2d0bb9dfbb6d21
git ls-files -s <file> → 100644 1fdc341ebe3653393b3ec04f0c2d0bb9dfbb6d21 0 <file>
git diff --quiet <file> → exit 0
工作区 blob 与 HEAD blob 完全一致,该文件在 git status 中的 M 标记是
Windows stat 缓存假阳性,未修改、未触碰 ✔
3. 删除的私有方法(真实数量:6)
逐文件 git diff 核对(- 行为真实删除):
| # | 文件 | 删除的方法签名 |
|---|---|---|
| 1 | controller/ApiController |
private String getRequestId(HttpServletRequest) |
| 2 | controller/AuthController |
private String getRequestId(HttpServletRequest) |
| 3 | controller/SystemController |
private String getRequestId(HttpServletRequest) |
| 4 | job/JobController |
private String getRequestId(HttpServletRequest) |
| 5 | scoringmonitoring/FairnessMonitoringController |
private Long userId() |
| 6 | scoringmonitoring/FairnessMonitoringController |
private String requestId(HttpServletRequest) |
计数澄清:getRequestId 名称的方法实际为 4 个;
FairnessMonitoringController 的 requestId 方法签名为 requestId(HttpServletRequest)
(无 get 前缀),不能被计入 getRequestId。
正确分解为 4 × getRequestId + 1 × userId + 1 × requestId = 6,
与上表逐文件清单一致 ✔
未为迎合任何数字修改生产代码 ✔
4. 独立接线变异证明
新增独立测试 A6AccessorWiringIndependentTest 将 CurrentUserAccessor、
RequestIdAccessor 全部替换为 mock,覆盖 5 个 Controller 的代表端点
(/、/health、/auth/me、GET /jobs、/admin/scoring/fairness/drift):
| 断言 | 结果 |
|---|---|
5 个端点响应体 requestId 均等于 mock 返回值 a6-indep-request-id |
通过 ✔ |
Accessor 返回 4242 时 service.drift(4242L) 被调用 |
通过 ✔ |
Accessor 返回 77L 时 overview/dimensions/rankingStability 收到 77L |
通过 ✔ |
Accessor 抛 InsufficientAuthenticationException → 401 且 service 零调用 |
通过 ✔ |
POST /jobs 企业侧身份经 CurrentUserAccessor.requireUserId() |
通过 ✔ |
变异检测含义:若任一 Controller 把 requestIdAccessor.get(request) 换回直读
request.getHeader("X-Request-Id"),响应体将不再出现 mock 值 → 该断言必然失败;
若 FairnessMonitoringController 换回直读 SecurityContextHolder,
4242/77 将不会传到 Service → 断言失败。接线真实生效,非空转 ✔
5. 独立测试质量审查
复验对象:
A6AccessorWiringIndependentTest(6 个用例)、A6IndependentVerificationTest(7 个用例)。
| 核对项 | 结果 |
|---|---|
| 公开端点未被错误要求 401 | ✔ 无 Token 断言仅覆盖 /auth/me 与 /admin/scoring/fairness/drift,/、/health、GET /jobs 明确排除在断言之外 |
| JobController 身份接线使用真正需要企业身份的端点 | ✔ 改用 POST /jobs(resolveCompanyId() 路径),非公开的 GET /jobs |
| Service 参数验证不是空转 | ✔ 断言具体数值(4242 / 77),而非仅 any() |
| requestId 断言未错误声称响应头来自 Accessor | ✔ 只断言「响应头非空且已规范化」+「响应体 requestId 与响应头同源」;响应头由 RequestIdFilter 产出,未作错误归属 |
| 副作用快照列真实存在 | ✔ 9 张表列名逐一对齐迁移文件(SCORING_RISK_EVENTS、SCORING_RISK_ACTIONS、JOBS、USERS、USER_ROLES、AUDIT_LOGS、NOTIFICATIONS、MESSAGES、CANDIDATE_DIMENSION_SCORES) |
| 未修改或放宽既有测试 | ✔ 既有测试文件零改动 |
| 未通过过度 mock 绕过目标 Controller | ✔ 目标 Controller 全部真实(@SpringBootTest + MockMvc),只 mock 下游 Service 与两个 Accessor |
5.1 独立测试首次运行的两处自身错误及修正
| # | 原断言 | 问题性质 | 修正 |
|---|---|---|---|
| 1 | 把公开端点路径 /、/health、GET /jobs 也断言为无 Token 401 |
测试自身错误(公开端点本不应 401),非生产缺陷 | 断言范围收窄为 /auth/me、/admin/scoring/fairness/drift 两个需认证端点 |
| 2 | 用公开的 GET /jobs 验证 JobController 的身份 Accessor 接线 |
测试自身错误(该端点不解析身份,断言无意义) | 改用 POST /jobs 企业侧端点 |
两处均为新测试自身缺陷,已修正;未修改生产代码,未修改任何既有测试。 修正后两个类合计 13/13 通过。
6. 身份边界与 401 / 403 / 404 分层
6.1 非法主体一律 401(真实 Accessor + 真实 SecurityFilterChain)
对主体 anonymousUser、not-a-number、99999999999999999999、0、-1,
以及 AnonymousAuthenticationToken 和完全无 Token:
| 断言 | 结果 |
|---|---|
| 状态码一律 401(不得 400/500) | ✔ |
响应体 code = 401 |
✔ |
message ∈ {authentication required, not authenticated} |
✔ |
message 不回显异常类名、java.、principal、anonymousUser、com.mydreamjob、NumberFormat |
✔ |
响应体无 stackTrace / trace |
✔ |
| 业务 Service 零调用 | ✔ |
合法数字主体(7)不被身份层拒绝 |
✔ |
6.2 401 / 403 / 404 分层未漂移
静态审查确认:A6 diff 未删除、未新增任何角色校验或资源归属校验;
JobController.resolveCompanyId() 与 jobService.assertOwnerOrThrow(companyId, jobId) 原样保留,
AuthController 的「已注销手机号 → HTTP 409 / code=9303」分支原样保留。
既有契约由既有测试继续守护且全部通过(例:AuthenticationBoundaryIntegrationTest
对 POST /jobs 断言 403 + ErrorCode.FORBIDDEN)。403 / 404 契约未漂移 ✔
7. requestId 语义
对 5 个 Controller 的代表端点,注入 X-Request-Id ∈
{a6v-legal-1, 全空白, 90 字符超长, <b>x</b>, a b}:
| 断言 | 结果 |
|---|---|
响应头 X-Request-Id 非空 |
✔ |
不含 <、>、空格、CR、LF |
✔ |
| 长度 ≤ 32 | ✔ |
响应体 "requestId":"<响应头同值>" |
✔ |
| 请求属性优先于原始 Header | ✔ |
| CR/LF 不进入规范化结果(6 组恶意输入) | ✔ |
8. 失败请求的业务副作用
illegalIdentity_producesNoBusinessWrite:对 9 张核心表按显式列 + 稳定主键排序做全量快照,
执行 5 组非法主体请求 + 1 组无 Token 请求后再次快照。
| 结果 |
|---|
| 9 张表逐字段零变化 ✔ |
9. FairnessMonitoringControllerTest 单跑:真实结果与准确根因
9.1 单跑命令与真实结果(不写成通过)
mvn -Dtest="FairnessMonitoringControllerTest" test
日志:docs/acceptance/r01-d4-2-20260916/a6v-fairness-repeat.log
Tests run: 7, Failures: 6, Errors: 0, Skipped: 0
BUILD FAILURE
6 个失败全部是 Status expected:<200|403|400> but was:<401>;
唯一通过的是 anonymous_isUnauthorized(不发 Token 的那个)。
9.2 逐项查明(任务书第 2 步的 6 个问题)
| # | 问题 | 结论 | 证据 |
|---|---|---|---|
| 1 | HTTP 实际状态 | 401,响应体 {"code":401,"message":"not authenticated","timestamp":""} |
MockHttpServletResponse 输出;Handler: Type = null(请求未进入任何 Controller) |
| 2 | Filter 为什么拒绝 | 不是验签失败,而是服务端会话校验的用户检查失败 | 日志:AuthSessionService - session validation rejected: user not usable → JwtAuthenticationFilter - JWT authentication rejected: session not active |
| 3 | 测试用户是否存在且 is_active = true |
行存在,但 is_active = 0(不是"不存在",也不是"active") |
见 §9.3 推理链 |
| 4 | auth_tokens 会话记录是否存在/未过期/未消费/未撤销 |
存在且有效;拒绝发生在读取会话记录之前 | TestAuthTokens.writeSession 自身执行 SELECT COUNT(*) FROM auth_tokens WHERE user_id=? AND session_id=? AND revoked=0 AND consumed_at IS NULL AND expired_at > ?,不可见即抛 IllegalStateException;日志中无该异常,说明会话行有效。而 isSessionActive() 先判用户、后判会话,拒绝文本是 "user not usable" 而非 "no active session" |
| 5 | JWT 的 sid/jti/sub/type/aud/iss 是否与库一致 |
一致;JWT 本身完全合法 | parseAndValidateAccessToken 未抛异常(否则拒绝路径不同)。日志中 Token 载荷:sub=1、type=access、sid=0073abf7-…、role=ADMIN、iss=mydreamjob、aud=["mydreamjob-api"]、iat=1577836800(2020-01-01)、exp=2523916800(2050-01-01),与夹具 FIXTURE_BASE/30 年 TTL 完全对应;sid 即写入 auth_tokens.session_id 的同值 |
| 6 | 是否真的与 user_roles 有关 |
无关 | AuthSessionService.isUsable(User) 只读 user.getActive();UserRepository.findById 无角色过滤;该路径从不查询 user_roles |
9.3 准确根因(证据链,取代此前未经证实的 user_roles 说法)
根因:TestAuthTokens 夹具只对"不存在的" users.id 执行 INSERT,对"已存在但 is_active=0"的行不做激活,而 V1_1 种子账号恰好占用 id=1/2/3 并被 V43 置为禁用。
证据链(逐步闭合):
V1_1__seed_data.sql按自增插入三名演示账号 → 占据users.id = 1 / 2 / 3(13800000001ADMIN、13800000002STUDENT、13800000003COMPANY)。V43__disable_legacy_seed_accounts.sql按原始指纹把这三行is_active置为 0。FairnessMonitoringControllerTest使用token(1L, "ADMIN")/token(2L, "STUDENT")→ 命中 id=1/2 这两个已被禁用的种子行。TestAuthTokens.writeSession()的行为是:SELECT COUNT(*) FROM users WHERE id = ? // id=1 已存在 → count = 1 if (count == null || count == 0) { INSERT ... is_active=1 } // → 被跳过,永不激活即夹具只补建缺失行,不修复既有行的启用状态。
users.created_at/updated_at在V1__base_schema.sql中均有DEFAULT CURRENT_TIMESTAMP,因此"若行不存在则夹具 INSERT 必然成功且is_active=1"成立。 既然实际观察到 401(而非 500/ERROR),只可能是行已存在且is_active=0—— 推理闭合。AuthSessionService.isSessionActive()第一步isUserUsable(userId)因此返回 false, 直接短路返回,从未走到会话记录检查,与日志文本完全吻合。
9.4 与 A6 生产改动的归因判定:无关
| 判据 | 内容 |
|---|---|
| 代码路径 | 401 由 JwtAuthenticationFilter / AuthSessionService 在请求进入 Controller 之前产生(Handler: Type = null)。A6 只改了 5 个 Controller 内部取 requestId / 取身份的写法,该路径上一行 A6 代码都未执行 |
| A6 diff 范围 | 仅 5 个 Controller + 文档;SecurityConfig、Filter、AuthSessionService、UserRepository、TestAuthTokens、Flyway 迁移零改动 |
| 失败面 | 7 个用例中,唯一不携带 Token 的用例通过;其余全部 401 —— 与「Token 认证链路在用户检查处失败」完全一致,与「Controller 被改写」不一致 |
9.5 为何全量顺序下不触发(机制已定位,非"偶然")
com.mydreamjob.controller.AuthIntegrationTest 的 @BeforeEach(源码第 97 行)执行
userRepository.deleteAll();,在共享的 integrationdb(无 @TestPropertySource 覆盖)上
清空全部用户,包括 V43 已禁用的 V1_1 种子行。
全量日志 docs/acceptance/r01-d4-2-20260916/a3-commit-cv.log 的执行顺序印证:
第 151767 行 [INFO] Running com.mydreamjob.controller.AuthIntegrationTest
第 247038 行 [INFO] Running com.mydreamjob.scoringmonitoring.FairnessMonitoringControllerTest
种子行被清空后,夹具第 4 步的 COUNT(*) 变为 0 → INSERT 补建 is_active=1 的用户 →
用户检查通过 → 该类在全量顺序下 7/7 通过。
因此:同一份代码,只因子夹具前置数据不同而结果不同 —— 属顺序依赖的夹具脆弱性, 不是被测代码的缺陷。
决定性对照实验(不新增任何文件、不修改任何代码,只改变前置数据):
mvn -Dtest="com.mydreamjob.controller.AuthIntegrationTest,FairnessMonitoringControllerTest" test
日志:docs/acceptance/r01-d4-2-20260916/a6v-fairness-mechanism.log
[INFO] Running com.mydreamjob.controller.AuthIntegrationTest
[INFO] Tests run: 6, Failures: 0, Errors: 0, Skipped: 0 -- in ...AuthIntegrationTest
[INFO] Running com.mydreamjob.scoringmonitoring.FairnessMonitoringControllerTest
[INFO] Tests run: 7, Failures: 0, Errors: 0, Skipped: 0 -- in ...FairnessMonitoringControllerTest
[INFO] BUILD SUCCESS
| 运行方式 | 同一份含 A6 的代码 | 结果 |
|---|---|---|
| 单独运行 Fairness | A6 在场 | 7 run / 6 failures |
| AuthIntegrationTest 先运行 | A6 在场 | 7 run / 0 failures |
全量 mvn clean verify |
A6 在场 | 7 run / 0 failures |
A6 代码在三行中完全相同,唯一变量是前置数据 —— 归因判定为夹具脆弱性的直接实验证据。
9.6 处置
- 不修改生产代码 ✔
- 不修改既有
FairnessMonitoringControllerTest✔ - 该缺陷列为 A6 提交后的独立纯测试修复:让
TestAuthTokens在行已存在时 显式确保is_active = 1(或改用不与 V1_1 种子账号冲突的 userId)。 - 未把该单跑命令写成通过 ✔
10. 全仓剩余统计(逐文件分类)
身份解析(SecurityContextHolder.getContext().getAuthentication /
Long.(valueOf|parseLong)(...getName()))—— 3 文件
| 文件 | 分类 |
|---|---|
security/CurrentUserAccessor |
基础设施(Accessor 自身) |
auth/JwtAuthenticationFilter |
基础设施(认证过滤器) |
deletion/DeletionGuardInterceptor |
基础设施(拦截器) |
requestId 解析(X-Request-Id 直读)—— 5 文件
| 文件 | 分类 |
|---|---|
audit/AuditContext |
基础设施 |
common/logging/RequestIdAccessor |
基础设施(Accessor 自身) |
common/logging/RequestIdFilter |
基础设施(过滤器) |
common/logging/RequestLoggingFilter |
基础设施 |
deletion/DeletionGuardInterceptor |
基础设施 |
业务 Controller 残留:身份解析 0、requestId 重复解析 0 ✔
11. 测试数
11.1 A6 定向
mvn -Dtest="R01A6RequestContextMigrationIntegrationTest,A6*Test,CurrentUserAccessorTest,RequestIdAccessorTest,RequestIdFilterTest" test
日志:docs/acceptance/r01-d4-2-20260916/a6v-v1-targeted.log
| 测试类 | Tests |
|---|---|
common.r01a6.R01A6RequestContextMigrationIntegrationTest |
7 |
common.r01a6.A6AccessorWiringProbeTest |
3 |
common.r01a6.A6AccessorWiringIndependentTest |
6 |
common.r01a6.A6IndependentVerificationTest |
7 |
security.CurrentUserAccessorTest |
11 |
common.logging.RequestIdAccessorTest |
16 |
common.logging.RequestIdFilterTest |
3 |
| 合计 | 53 / 53,Failures=0、Errors=0、Skipped=0,BUILD SUCCESS ✔ |
11.2 A6 关联
mvn -Dtest="*ApiController*Test,*AuthController*Test,*SystemController*Test,*JobController*Test,*Security*Test,*RequestId*Test" test
日志:docs/acceptance/r01-d4-2-20260916/a6v-v2-related.log
| 测试类 | Tests |
|---|---|
auth.SmsVerificationSecurityTest |
10 |
common.logging.RequestIdAccessorTest |
16 |
common.logging.RequestIdFilterTest |
3 |
| 合计 | 29 / 29,Failures=0、Errors=0、Skipped=0,BUILD SUCCESS ✔ |
说明:该命令的
*ApiController*Test、*AuthController*Test、*SystemController*Test、*JobController*Test四个模式在本仓库不存在同名测试类(find全量核对: 无ApiControllerTest/SystemControllerTest/JobControllerTest/AuthControllerTest), 故只命中上表 3 个类。这 5 个 Controller 的既有覆盖由mvn clean verify全量承担(含AuthenticationBoundaryIntegrationTest对POST /jobs的 403 断言、controller.AuthIntegrationTest的登录/刷新/登出全链路等)。未因此放宽任何断言。
11.3 全量
mvn clean verify
日志:docs/acceptance/r01-d4-2-20260916/a6v-v3-verify.log
[INFO] Tests run: 2278, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
| 项 | 数值 |
|---|---|
| 全量测试总数 | 2278 |
| Failures | 0 ✔ |
| Errors | 0 ✔ |
| Skipped | 0 ✔ |
| 原实施基线 | 2265 |
| 独立新增 | 13(A6AccessorWiringIndependentTest 6 + A6IndependentVerificationTest 7) |
| 2265 + 13 | 2278 ✔ 与 Surefire 汇总完全一致 |
无需要按 Surefire XML 另行解释的差异 —— 汇总行与预期逐位吻合。
11.4 FairnessMonitoringControllerTest 单跑(不计入通过)
| 命令 | 结果 |
|---|---|
mvn -Dtest="FairnessMonitoringControllerTest" test |
7 run / 6 failures / 0 errors / 0 skipped,BUILD FAILURE |
按任务书要求:该单跑命令不写成通过,其失败与准确根因见 §9。
12. OpenAPI
python ..\docs\verify_openapi.py
→ paths: 176 / operations: 214
→ All checks passed!
git diff -- docs/openapi-rc.json → 空,openapi-rc.json 零修改 ✔
13. Flyway / ErrorCode / DTO / 接口
| 项 | 结果 |
|---|---|
git diff --name-only -- mydreamjob-server/src/main/resources/db/migration |
空 —— V1–V45 未改动 ✔ |
ErrorCode |
未修改 ✔ |
| DTO / 响应体结构 | 未修改(仅取值来源改变) ✔ |
| 接口(路径 / 方法 / 参数) | 未新增、未删除、未改签名 ✔ |
依赖(pom.xml) |
未修改 ✔ |
14. Git 状态
| 项 | 结果 |
|---|---|
| 分支 | refactor/r01-structure-performance |
| HEAD | 534afe686b966f175bcaf4af0726cec061092050(未变) |
git diff --cached --name-only |
空 |
git diff --check |
仅 LF/CRLF 警告,exit 0 |
| 已暂存 / 已提交 / 已推送 | 均无 ✔(git log -1 仍为 A5 提交 534afe6) |
| 与远端关系 | ## refactor/r01-structure-performance...origin/refactor/r01-structure-performance(无 ahead/behind) |
| 历史未跟踪文件 | 未删除 ✔ |
| 本轮验收日志 | 位于 docs/acceptance/r01-d4-2-20260916/,该路径被 .gitignore:43 **/*.log 忽略(docs/acceptance 受版本管理的文件数为 0),不会进入任何提交 ✔ |
| 残留 Java/Maven 进程 | 无 ✔ |
15. 明确声明
- 未暂存、未提交、未推送。
- 未修改任何生产代码;未修改、删除或放宽任何既有测试断言;
未修改
GlobalExceptionHandler、SecurityConfig、Filter、Accessor、ErrorCode、DTO、 OpenAPI、Flyway 迁移与 V1–V45。 - 本轮仅新增
docs/r01-a6-independent-verification-20260917.md与验收日志; 独立测试自身的两处缺陷已就地修正(§5.1),未触碰生产代码与既有测试。 - 未进入 R01-B;A 系列到此结束,不再设置 A7/A8。
- 本轮未启动 Docker,未伪造真实 MySQL 结论。
R01-A6独立代码复验通过,可以进入精确暂存与独立提交检查点;A系列到此结束,不再设置A7/A8。FairnessMonitoringControllerTest的单跑夹具缺陷须在A6提交后以独立纯测试提交修复。