轻松聘AI开发
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.
 
 
 

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。