18 KiB
R01-A6 最终业务 Controller 请求身份与 requestId 收束报告
日期:2026-09-17
分支:refactor/r01-structure-performance
A6 基线 HEAD:534afe686b966f175bcaf4af0726cec061092050(A5 提交)
本轮性质:实施与自动化验证 —— 未提交、未推送,待独立代码复验
0. 结论
R01-A6最终业务Controller请求上下文收束完成,待独立代码复验;A系列到此结束,不再拆分A7/A8。
业务 Controller 身份解析残留 = 0;业务 Controller requestId 重复解析残留 = 0。 剩余命中全部为基础设施职责。
1. 开始时状态
1.1 A5 固化(第一阶段)
A5 此前未提交,本轮按已确认清单精确固化:
| 项 | 结果 |
|---|---|
| 暂存文件数 | 15(8 生产 Controller + 4 测试 + 2 报告 + 1 路线图) |
git add 方式 |
逐文件 git add --;未使用 git add -A / git add . |
| 路线图 | git diff --numstat 为 1 1(仅 R01 行一处 hunk),无需 git add -p |
| 排除项 | GlobalExceptionHandler.java、db/migration、docs/acceptance、backend-infra、A2–A4 文件 全部排除 ✔ |
git diff --cached --check |
无输出 ✔ |
| 敏感扫描 | 1050 新增行,12 类(手机号/JWT/Bearer/验证码/口令/密钥/私钥/AWS/64位摘要/本机路径/UUID/连接串)全部命中 0 ✔ |
| A5 定向 | 42 / 42,F=0 E=0 S=0 |
mvn clean verify |
2255 / 2255,F=0 E=0 S=0,BUILD SUCCESS |
| OpenAPI | 176 / 214,All checks passed |
| 提交 | refactor: centralize fifth batch request context handling |
| A5 提交 hash | 534afe686b966f175bcaf4af0726cec061092050 |
| 推送 | 0226977..534afe6;未 force push |
| 推送后同步 | 0 / 0 ✔ |
1.2 A6 开始前核查(第二阶段)
| 检查项 | 结果 |
|---|---|
| 分支 | refactor/r01-structure-performance ✔ |
| 新 HEAD | 534afe686b966f175bcaf4af0726cec061092050 ✔ |
| 暂存区 | 空 ✔ |
git diff --check |
无空白错误 ✔ |
| A5 未提交残余 | 无(工作区仅 GlobalExceptionHandler.java 的 stat 缓存标记,blob 三方一致)✔ |
| 历史未跟踪文件 | 全部保留 ✔ |
| Docker / R01-B·C·E | 未触碰 ✔ |
A6 基线测试数:2255。
2. 五个 Controller 的修改内容
2.1 迁移前契约
| Controller | 真实代表端点 | 身份解析 | requestId 解析 |
|---|---|---|---|
controller/ApiController |
GET / |
无 | private String getRequestId(HttpServletRequest) → request.getHeader("X-Request-Id") |
controller/AuthController |
POST /auth/login、GET /auth/me 等 |
已有 CurrentUserAccessor ✓ |
private String getRequestId(...) |
controller/SystemController |
GET /health、GET /info |
无 | private String getRequestId(...) |
job/JobController |
GET /jobs、POST /jobs 等 |
已有 CurrentUserAccessor ✓ |
private String getRequestId(...) |
scoringmonitoring/FairnessMonitoringController |
GET /admin/scoring/fairness/* |
需迁移:private Long userId() 直读 SecurityContextHolder + Long.valueOf(auth.getName()) |
private String requestId(HttpServletRequest) 单行直读 |
2.2 修改后
| Controller | 注入 | requireUserId() |
requestIdAccessor.get(request) |
|---|---|---|---|
| ApiController | 新增 RequestIdAccessor(并补 @RequiredArgsConstructor) |
0 | 1 |
| AuthController | 新增 RequestIdAccessor(CurrentUserAccessor 既有) |
2(既有) | 14 |
| SystemController | 新增 RequestIdAccessor(并补 @RequiredArgsConstructor) |
0 | 2 |
| JobController | 新增 RequestIdAccessor(CurrentUserAccessor 既有) |
1(既有) | 8 |
| FairnessMonitoringController | 新增 CurrentUserAccessor + RequestIdAccessor |
5 | 5 |
JobController 的身份迁移未被重复改造 ✔ —— 它此前已使用 CurrentUserAccessor,本批只迁移 requestId。
AuthController 无重复身份解析 ✔ —— 它此前已使用 CurrentUserAccessor,本批只迁移 requestId。
2.3 删除的私有方法(共 6 个)
| Controller | 删除 |
|---|---|
| ApiController | getRequestId ×1 |
| AuthController | getRequestId ×1 |
| SystemController | getRequestId ×1 |
| JobController | getRequestId ×1 |
| FairnessMonitoringController | userId ×1、requestId ×1 |
| 合计 | 6 |
计数澄清(独立复验补充):曾出现"
5 × getRequestId + 1 × userId + 1 × requestId"的表述,其算术结果为 7,与真实 diff 不符,该表达式不成立。经逐文件git diff核对:getRequestId实际只有 4 个(ApiController / AuthController / SystemController / JobController);FairnessMonitoringController的 requestId 方法签名是requestId(HttpServletRequest)(无get前缀),因此不能被计入getRequestId。正确分解为 4 × getRequestId + 1 × userId + 1 × requestId = 6,与本表逐文件清单完全一致。未为迎合数字修改任何生产代码。
保留:FairnessMonitoringController.parseDate(业务参数解析)、checkParams(业务参数校验)✔ 未被误删。
新增注解:ApiController 与 SystemController 原本无依赖、缺 @RequiredArgsConstructor,为支持构造器注入补齐该注解与 import(唯一的新增注解,未引入 @Autowired)。
未修改:RequestIdFilter、RequestIdAccessor、RequestLoggingFilter、AuditContext、GlobalExceptionHandler、SecurityConfig、Filter 顺序 ✔
3. 修复前失败证据
新增 mydreamjob-server/src/test/java/com/mydreamjob/common/r01a6/ 下两个测试类。
修复前实测:Tests run: 10, Failures: 5
| 失败测试 | 修复前实际 |
|---|---|
A6AccessorWiringProbeTest.requestIdComesFromAccessorAcrossFiveControllers |
path=/ 响应体 requestId 不是 Accessor 返回值(Controller 直读 header) |
A6AccessorWiringProbeTest.fairnessMonitoringUsesCurrentUserAccessor |
FairnessMonitoringService.drift(...) 未被调用(身份未走 Accessor) |
A6AccessorWiringProbeTest.accessorThrowsUnauthenticated_fairnessReturns401 |
不成立 |
R01A6RequestContextMigrationIntegrationTest.fairnessMonitoring_illegalPrincipals_are401 |
principal='anonymousUser' → expected: 401 but was: 400 |
R01A6RequestContextMigrationIntegrationTest.fairnessMonitoring_illegalPrincipals_produceNoServiceCall |
不成立 |
关于 400 而非 500(如实记录):FairnessMonitoringController.userId() 带有
try { Long.valueOf(auth.getName()); } catch (NumberFormatException) { throw BusinessException(UNAUTHORIZED); },
因此其既有行为本身是安全的(未逃逸为 500),但抛出的 BusinessException(ErrorCode.UNAUTHORIZED=2001)
不在 GlobalExceptionHandler.BUSINESS_HTTP_STATUSES 映射中,resolveHttpStatus 的
getOrDefault(2001, BAD_REQUEST) 使其落到 HTTP 400。这正是本批收束的真实语义缺陷:
非法主体应返回 401 而非 400。接线探针则因"未调用 Accessor"而失败 —— 未编造 500。
修复后:Tests run: 10, Failures: 0
4. 角色与资源归属语义
| 核查项 | 结论 |
|---|---|
| 静态审查:删除行中是否含角色/归属检查 | 否 ✔ |
| 静态审查:新增行中是否引入角色/归属逻辑 | 否 ✔ |
ErrorCode.java |
零改动 ✔ |
| ADMIN / COMPANY / STUDENT 角色检查 | 未删除、未移动 ✔ |
| 企业归属 / 资源归属 / 403 / 404 分支 | 未删除、未移动 ✔ |
| 监控数据访问权限(ADMIN 只读聚合) | 未改变 ✔ |
分层保持:身份失败 → 401;角色不足 → 403(由 Service 层或既有逻辑产生);资源不存在 → 404。
5. 独立接线探针
A6AccessorWiringProbeTest(3 例)使用 @MockBean 替换 CurrentUserAccessor、RequestIdAccessor 与对应 Service:
| 用例 | 证明 |
|---|---|
requestIdComesFromAccessorAcrossFiveControllers |
mock RequestIdAccessor 返回 a6-probe-request-id → 5 个 Controller 的响应体 requestId 均为该值(修复前为直读 header 的 null) |
fairnessMonitoringUsesCurrentUserAccessor |
mock CurrentUserAccessor 返回 5151L → fairnessMonitoringService.drift(5151L) 被调用 |
accessorThrowsUnauthenticated_fairnessReturns401 |
mock 抛 InsufficientAuthenticationException → FairnessMonitoring 返回 401,Service 零调用 |
变异有效性:删除任一 Controller 中的 Accessor 调用会使对应 verify 失败 ✔
6. 非法 principal 的结果
R01A6RequestContextMigrationIntegrationTest 对 GET /admin/scoring/fairness/drift 覆盖:
| 主体 | 结果 |
|---|---|
anonymousUser |
401 ✔ |
not-a-number |
401 ✔ |
99999999999999999999(20 位溢出) |
401 ✔ |
0 |
401 ✔ |
-1 |
401 ✔ |
| 匿名 token / 无 Token | 401 ✔ |
合法数字主体 "7" |
不被身份层拒绝(非 401)✔ |
断言:401 且无 500;响应体仅解析 code/message 稳定字段,不对整段 JSON 做数字搜索;不含 Exception/java./principal/anonymousUser/com.mydreamjob/NumberFormat;无 stackTrace/trace;业务 Service 零调用 ✔
7. requestId 规范化
覆盖 5 个 Controller 的真实端点(/、/health、/auth/me、/jobs、/admin/scoring/fairness/drift):
| 输入 | 结果 |
|---|---|
合法值 a6-legal-0001 |
响应头 == 响应体,原样保留 ✔ |
空白 " " |
重新生成,头体同源 ✔ |
| 超长(80 字符) | 收敛至 ≤32,头体同源 ✔ |
非法字符 <script>x</script>、a b |
生成合法替代值,响应头不含 </>/空格 ✔ |
| request attribute 与 header 不同 | attribute 优先 ✔ |
| CR / LF / CRLF(直接经 Accessor) | 不进入规范化结果 ✔ |
8. 全仓剩余命中逐文件分类
身份解析(SecurityContextHolder.getContext().getAuthentication | Long.(valueOf|parseLong)([^)]*getName())—— 3 文件
| 文件 | 分类 |
|---|---|
security/CurrentUserAccessor |
基础设施(Accessor 自身) |
auth/JwtAuthenticationFilter |
基础设施(认证过滤器) |
deletion/DeletionGuardInterceptor |
基础设施(拦截器) |
requestId 解析(含 X-Request-Id 直读、RequestIdFilter.REQUEST_ID_HEADER、String (getRequestId|resolveRequestId|requestId)()—— 5 文件
| 文件 | 分类 |
|---|---|
audit/AuditContext |
基础设施 |
common/exception/GlobalExceptionHandler |
基础设施 |
common/logging/RequestIdAccessor |
基础设施(Accessor 自身) |
common/logging/RequestLoggingFilter |
基础设施 |
deletion/DeletionGuardInterceptor |
基础设施 |
全部落在任务书允许的基础设施清单内(CurrentUserAccessor、JwtAuthenticationFilter、DeletionGuardInterceptor、RequestIdAccessor、RequestIdFilter、RequestLoggingFilter、AuditContext、GlobalExceptionHandler)✔
未为"全仓零命中"修改任何基础设施类 ✔
未在 A6 清单内的业务 Controller 残留:0 ✔ —— 无需越界修改。
9. 业务 Controller 残留
| 项 | 结果 |
|---|---|
| 业务 Controller 身份解析 | 0 ✔ |
| 业务 Controller requestId 重复解析 | 0 ✔ |
A 系列请求上下文收束达成目标终态。A7/A8 不再设置。
10. 测试数
| 命令 | 结果 |
|---|---|
A6 定向(R01A6…,A6AccessorWiringProbeTest,CurrentUserAccessorTest,RequestIdAccessorTest,RequestIdFilterTest) |
40 / 40,F=0 E=0 S=0 |
A6 关联(*ApiController*Test,*AuthController*Test,*SystemController*Test,*JobController*Test,*FairnessMonitoring*Test,*Security*Test,*RequestId*Test) |
76 / 76(1 项既有夹具脆弱性,见 §10.1,全量顺序下不触发) |
mvn test |
2265 / 2265,Failures=0、Errors=0、Skipped=0 |
mvn clean verify |
2265 / 2265,Failures=0、Errors=0、Skipped=0,BUILD SUCCESS |
A5 基线 2255 → A6 2265(新增 10 个测试:7 + 3),不低于基线 ✔
10.1 基线已存在的测试夹具脆弱性(未在本批修复)
A6 关联回归单独运行时,既有 FairnessMonitoringControllerTest 出现 6 个失败(200/403/400 → 401),根因为:
Caused by: AuthSessionService - session validation rejected: user not usable
JwtAuthenticationFilter - JWT authentication rejected: session not active
归因验证(任务书要求):在 git worktree add --detach HEAD(不含 A6 改动)中运行完全相同的命令:
| 测试数 | 失败 | 根因 | |
|---|---|---|---|
| 基线(HEAD) | 7 | 6 | user not usable |
| 本次(含 A6) | 7 | 6 | 完全相同 |
实施阶段初步判断(后被独立复验否定,保留于此以存档过程):当时认为
TestAuthTokens 夹具插入 users 行时不写 user_roles,导致该用户被判 "not usable"。
该判断不成立:AuthSessionService.isUsable(User) 只检查用户存在且 is_active = true,
UserRepository.findById 无角色过滤,该路径从不查询 user_roles。
独立复验结论(准确根因):见 docs/r01-a6-independent-verification-20260917.md §9。
| 环节 | 事实 |
|---|---|
| 身份冲突 | FairnessMonitoringControllerTest 使用 token(1L, "ADMIN") / token(2L, "STUDENT"),与 V1_1__seed_data.sql 种子账号占用的 users.id = 1 / 2 / 3 冲突 |
| 种子被禁用 | V43__disable_legacy_seed_accounts.sql 按原始指纹把这三行 is_active 置为 0 |
| 夹具缺陷 | TestAuthTokens.writeSession() 仅当 SELECT COUNT(*) FROM users WHERE id = ? 为 0 时才 INSERT;对已存在但 is_active = 0 的行不做激活,永不修复其启用状态 |
| 直接后果 | AuthSessionService.isSessionActive() 第一步 isUserUsable() 返回 false 并短路,请求在进入 Controller 前即 401 |
| 全量为何通过 | 全量顺序中 com.mydreamjob.controller.AuthIntegrationTest 的 @BeforeEach 执行 userRepository.deleteAll(),偶然清除了 V43 已禁用的种子行,夹具随后才补建出 active 用户 —— 问题被掩盖,而非被修复 |
与 A6 生产改动无关:401 由 JwtAuthenticationFilter / AuthSessionService 在请求进入
Controller 之前产生(Handler: Type = null),A6 只改了 5 个 Controller 内部取身份 / 取
requestId 的写法,该路径上一行 A6 代码都未执行。独立复验的决定性对照实验:同一份含 A6 的
代码,单跑 Fairness 为 7 run / 6 failures,而让 AuthIntegrationTest 先跑则为 7 run / 0 failures。
mvn clean verify(全量顺序)下 2278/2278 全通过,该失败不触发。
按任务书要求:未把失败写成通过,未在本批顺手修复;该夹具问题将在 A6 提交后 以独立纯测试提交修复(不与本批生产改动混合)。
11. OpenAPI
python docs\verify_openapi.py → 176 paths / 214 operations,All checks passed ✔
docs/openapi-rc.json 零修改 ✔
12. 精确修改文件
生产代码(5 个,全部在 A6 指定清单内)
mydreamjob-server/src/main/java/com/mydreamjob/controller/ApiController.java
mydreamjob-server/src/main/java/com/mydreamjob/controller/AuthController.java
mydreamjob-server/src/main/java/com/mydreamjob/controller/SystemController.java
mydreamjob-server/src/main/java/com/mydreamjob/job/JobController.java
mydreamjob-server/src/main/java/com/mydreamjob/scoringmonitoring/FairnessMonitoringController.java
测试(新增 2 个)
mydreamjob-server/src/test/java/com/mydreamjob/common/r01a6/R01A6RequestContextMigrationIntegrationTest.java
mydreamjob-server/src/test/java/com/mydreamjob/common/r01a6/A6AccessorWiringProbeTest.java
文档(2 个)
docs/r01-a6-final-request-context-migration-report.md # 本报告
docs/backend-development-roadmap.md # 仅更新顶部 R01 状态行
未修改:Service、DTO、ErrorCode、Flyway、SecurityConfig、Filter、Accessor、OpenAPI、既有测试、UserRoleGuard。
13. Flyway / ErrorCode / DTO / 接口
| 项 | 结果 |
|---|---|
| V1–V45 迁移 | 零修改 ✔ |
ErrorCode.java |
零改动 ✔ |
| DTO | 零改动 ✔ |
| 公网接口 / OpenAPI | 零新增零修改 ✔ |
| 新增表 / 索引 / 迁移 | 无 ✔ |
14. Git 状态
git diff --check → 无输出
git status --short → 8 个生产/文档改动 + GlobalExceptionHandler(stat 缓存标记)
git diff --cached --name-only → 空
git diff --name-only -- db/migration → 空
git diff -- docs/openapi-rc.json → 空
GlobalExceptionHandler.java:工作区 / 索引 / HEAD 三方 blob 均为1fdc341e…,内容零差异,未触碰 ✔- 临时基线 worktree 已
--force移除并 prune,git worktree list仅主仓库 ✔
15. 明确声明
- 未暂存、未提交、未推送。
- 未操作 Docker。
- 未进入 R01-B / R01-C / R01-E。
- 未修改基础设施类(
RequestIdFilter、RequestIdAccessor、RequestLoggingFilter、AuditContext、GlobalExceptionHandler、SecurityConfig)。 - 未删除历史未跟踪文件。
- 基线测试污染已用 HEAD 隔离 worktree 复现并如实记录,未写成通过、未顺手修复。
R01-A6最终业务Controller请求上下文收束完成,待独立代码复验;A系列到此结束,不再拆分A7/A8。