13 KiB
R01-A2 第二批:请求身份与 requestId 收束报告
日期:2026-09-17
分支:refactor/r01-structure-performance
基线 HEAD:52c8026b2e56dae7dbfae6a828a9efe23e180445
范围:仅 8 个 Controller(ApplicationController、CompanyController、MessagingController、OfferController、InterviewController、CandidateScoringController、CandidateScoreCardController、CompanyScoringDashboardController)
0. 结论
R01-A2第二批请求身份与requestId收束完成,待独立代码复验。
8 个 Controller 的身份与 requestId 解析已统一收束到既有 CurrentUserAccessor / RequestIdAccessor;非法主体由逃逸为 HTTP 500 改为统一 401;requestId 不再由 Controller 直读原始请求头。
1. 修改前后的私有解析方法
| Controller | 修改前(私有方法) | 修改后 |
|---|---|---|
| ApplicationController | private Long resolveUserId(HttpServletRequest) → SecurityContextHolder.getContext().getAuthentication();auth==null || !isAuthenticated() → BusinessException(UNAUTHORIZED);return Long.valueOf(auth.getName())private String getRequestId(HttpServletRequest) → request.getHeader("X-Request-Id") |
currentUserAccessor.requireUserId()(10 处)requestIdAccessor.get(request)(9 处) |
| CompanyController | 同上(私有 resolveUserId + getRequestId) |
requireUserId() ×3;requestIdAccessor.get(request) ×6 |
| MessagingController | 同上 | requireUserId() ×7;requestIdAccessor.get(request) ×7 |
| OfferController | 同上 | requireUserId() ×13;requestIdAccessor.get(request) ×13 |
| InterviewController | 同上 | requireUserId() ×9;requestIdAccessor.get(request) ×9 |
| CandidateScoringController | private Long resolveUserId()(无参)+ getRequestId |
requireUserId() ×4;requestIdAccessor.get(request) ×4 |
| CandidateScoreCardController | private Long resolveUserId()(无参)+ getRequestId |
requireUserId() ×3;requestIdAccessor.get(request) ×3 |
| CompanyScoringDashboardController | private Long resolveUserId()(无参)+ getRequestId |
requireUserId() ×7;requestIdAccessor.get(request) ×7 |
共删除私有解析方法 16 个(8 个 resolveUserId + 8 个 getRequestId),并移除随之不再使用的 import:org.springframework.security.core.Authentication、org.springframework.security.core.context.SecurityContextHolder(后 3 个 Controller)。
新增字段(每个 Controller):
/** R01-A2:身份只来自安全上下文,不接受请求参数传入的 userId。 */
private final com.mydreamjob.security.CurrentUserAccessor currentUserAccessor;
/** R01-A2:requestId 只来自过滤器规范化后的唯一产出点。 */
private final com.mydreamjob.common.logging.RequestIdAccessor requestIdAccessor;
未做:未把 userId 加入请求参数;未调整角色检查调用位置;未调整归属判断;未复制第二套解析器。
2. 端点 401/403/404 语义
2.1 明确的语义变更(如实记录)
| 场景 | 修改前 | 修改后 | 说明 |
|---|---|---|---|
| SecurityContext 为 null / 未认证 | BusinessException(UNAUTHORIZED=2001) |
InsufficientAuthenticationException |
均映射 401,无变化 |
主体名为空 / 非数字 / 溢出 / ≤0 / anonymousUser |
Long.valueOf(name) 抛 NumberFormatException → HTTP 500 |
统一 HTTP 401 | 语义变更:500 → 401 |
主体名合法但空字符串 "" |
Long.valueOf("") → 500 |
401 | 同上 |
依据:ErrorCode.UNAUTHORIZED(2001) 不在 GlobalExceptionHandler.BUSINESS_HTTP_STATUSES 的任何 register 列表中,resolveHttpStatus 的 getOrDefault(2001, BAD_REQUEST) 使其落到 400;而空/非法名称在 Long.valueOf 处直接抛 NumberFormatException 逃逸为 500(修复前特征测试已实测复现)。
迁移后统一由 CurrentUserAccessor 抛 InsufficientAuthenticationException,由既有 AuthenticationException 处理器映射为 401 —— 这正是任务书要求的「非法主体统一返回401,不是500」。
说明:修改前该路径实际为 400 或 500(取决于主体名是否合法可解析),并非 401。因此本次变更不触及原本就是 401/403/404 的场景。
2.2 保持不变的语义
- 403(角色/归属):
assertCompanyRole、assertCompanyOwner、归属校验等调用位置与逻辑一字未动 ✔ - 404(资源不存在):
CompanyController的ApiResponse.error(404, "company not found", …)等保持不变 ✔ - 业务错误码:未修改
ErrorCode、未修改resolveHttpStatus、未修改GlobalExceptionHandler✔ - filter chain 层 401:无 token 的请求由
SecurityFilterChain在 Controller 之前拒绝,行为不变(既有list_noToken_shouldReturn401等断言持续通过)✔
3. 修复前失败测试
新增 RequestContextMigrationIntegrationTest(mydreamjob-server/src/test/java/com/mydreamjob/common/r01a2/),使用真实 SecurityFilterChain + MockMvc(@SpringBootTest + @AutoConfigureMockMvc)。
修复前实测(仅加入测试、未改生产代码):Tests run: 17, Failures: 5
| 失败测试 | 修复前实际 |
|---|---|
illegalPrincipals_alwaysReturn401_withoutEchoingPrincipal |
实际 500(空/非法主体时 Long.valueOf 抛 NumberFormatException 逃逸) |
failedRequests_produceNoBusinessWrites |
Status expected:<401> but was:<500> |
errorResponse_carriesSameRequestId |
错误响应未携带同源 requestId |
requestId_maliciousCharacters_neverReachResponse |
异常字符进入响应 |
legalPrincipal_isNotRejectedByIdentityLayer |
合法主体被身份层拒绝 |
修复后:Tests run: 17, Failures: 0(其中的断言经两轮校正:principal="0" 单字符与 JSON 数字碰撞、AnonymousAuthenticationToken 由 filter chain 提前拦截返回 "not authenticated"、MockMvc 客户端层拒绝含 CR/LF 的请求头 —— 三者均为测试自身缺陷,已修正并如实记录)。
4. requestId 同源证据
RequestIdFilter 是 requestId 的唯一产出点:规范化后同时写入请求属性 RequestIdFilter.REQUEST_ID_ATTRIBUTE、MDC、响应头,并用请求包装器覆盖 X-Request-Id。RequestIdAccessor.get(request) 优先读取该请求属性,兜底仍走同一 normalizeRequestId。
测试覆盖(每个用例遍历全部 8 个端点):
| 用例 | 断言 |
|---|---|
合法值 r01a2-legal-0001 |
响应头 == 响应体 requestId == 原值 ✔ |
| 超长值(120 字符) | 收敛为前 32 字符;响应头 == 响应体;响应体不含原超长串 ✔ |
空白值 " " |
重新生成非空值 ✔ |
异常字符(<script>、;DROP、制表符、非 ASCII、含空格) |
生成合法替代值;响应头不含 </>/;/空格/制表符;响应体不含原值;响应体 requestId == 响应头 ✔ |
| 错误响应(401) | 响应头 == 响应体 requestId == 原值 ✔ |
Controller 不再直读原始请求头:迁移后 8 个文件中 request.getHeader("X-Request-Id") 命中数为 0(逐文件核查见 §5)。
5. 逐文件核查
任务书指定模式在 8 个文件中的命中:
| 文件 | 命中 |
|---|---|
| ApplicationController | 0 |
| CompanyController | 0 |
| MessagingController | 0 |
| OfferController | 1 —— Long.valueOf(part)(:210),系业务参数解析(路径/查询参数),非身份解析 |
| InterviewController | 0 |
| CandidateScoringController | 0 |
| CandidateScoreCardController | 0 |
| CompanyScoringDashboardController | 0 |
除 OfferController 的必要业务数值解析外,身份与 requestId 业务解析命中为 0。
6. 全仓剩余数量(如实报告,未声称全仓完成)
| 模式 | 迁移前(HEAD) | 迁移后(工作区) | 减少 |
|---|---|---|---|
SecurityContextHolder.getContext().getAuthentication | Long.(valueOf|parseLong)(.*getName |
36 个文件 | 28 个文件 | 8 |
getRequestId | resolveRequestId | request.getHeader("X-Request-Id") |
51 个文件 | 43 个文件 | 8 |
减少量恰好等于本次处理的 8 个 Controller。
剩余 28 / 43 个文件尚未迁移,其中包含:
- 基础设施类(合理保留):
security/CurrentUserAccessor.java(Accessor 自身)、auth/JwtAuthenticationFilter.java(认证过滤器)、deletion/DeletionGuardInterceptor.java(拦截器)、common/exception/GlobalExceptionHandler.java、common/logging/RequestLoggingFilter.java - 待后续批次迁移的业务 Controller(约 23 个):
bidirectionalmatch/*、careerprogress/、feedback/、interviewscore/、job/FavoriteController、match/、notification/、recommendation/、review/*、risk/、safety/*、scoringmonitoring/、talentdiscovery/、talentinvitation/、verification/、candidatescore/governance/、account/等 - Service 层:
audit/*、messaging/MessagingService、offer/OfferService、review/*Service、talentdiscovery/*Service、talentinvitation/*Service、bidirectionalmatch/BidirectionalMatchService
本次未覆盖它们,也未修改它们。
7. 测试数量与 OpenAPI
| 命令 | 结果 |
|---|---|
| 定向(11 个类:新增特征测试 + 8 个 Controller 测试 + 3 个既有 Accessor/Filter 测试) | 161 / 161,F=0 E=0 S=0 |
第二批(*Application*Test,*Company*Test,*Messaging*Test,*Offer*Test,*Interview*Test,*CandidateScoring*Test,*Dashboard*Test,*RequestContext*Test,*RequestId*Test) |
630 / 630,F=0 E=0 S=0,BUILD SUCCESS |
mvn clean verify |
2185 / 2185,Failures=0、Errors=0、Skipped=0,BUILD SUCCESS |
python docs\verify_openapi.py |
176 paths / 214 operations,All checks passed |
较上一轮(2176)新增 9 个测试,全部为本次新增的 RequestContextMigrationIntegrationTest。
- 未修改
docs/openapi-rc.json✔ - 未出现新的数据库副作用:本次改动只涉及 Controller 层解析与测试,未新增/修改任何 Flyway 迁移、实体或仓储调用 ✔
8. 精确修改文件
生产代码(8 个,全部在任务书指定清单内)
mydreamjob-server/src/main/java/com/mydreamjob/application/ApplicationController.java
mydreamjob-server/src/main/java/com/mydreamjob/company/CompanyController.java
mydreamjob-server/src/main/java/com/mydreamjob/messaging/MessagingController.java
mydreamjob-server/src/main/java/com/mydreamjob/offer/OfferController.java
mydreamjob-server/src/main/java/com/mydreamjob/interview/InterviewController.java
mydreamjob-server/src/main/java/com/mydreamjob/candidatescore/CandidateScoringController.java
mydreamjob-server/src/main/java/com/mydreamjob/candidatescore/CandidateScoreCardController.java
mydreamjob-server/src/main/java/com/mydreamjob/candidatescore/dashboard/CompanyScoringDashboardController.java
测试(新增 1 个)
mydreamjob-server/src/test/java/com/mydreamjob/common/r01a2/RequestContextMigrationIntegrationTest.java
文档(1 个)
docs/backend-development-roadmap.md # 仅更新顶部 R01 状态表,未重写历史章节
未修改:ErrorCode、Flyway 迁移、openapi-rc.json、DTO、Service、角色检查、资源归属、UserRoleGuard、GlobalExceptionHandler。
关于
GlobalExceptionHandler.java:git status显示M,但工作区 / 索引 / HEAD 的 blob hash 均为1fdc341ebe3653393b3ec04f0c2d0bb9dfbb6d21,内容零差异,系 Git 在 Windows 上的文件系统 stat 缓存失效所致。未修改、未提交。
9. Git 状态
分支:refactor/r01-structure-performance
HEAD:52c8026b2e56dae7dbfae6a828a9efe23e180445(未变)
暂存区:空
git add -A / commit / push:均未执行
reset / restore / checkout / clean:均未执行
Docker:未操作
历史未跟踪文件(238 个,含 docs/acceptance、backend-infra 等):完整保留
10. 明确声明
- 未暂存、未提交、未推送。
- 未修改角色检查、资源归属、Service、DTO、ErrorCode、Flyway、接口或 OpenAPI。
- 未进入
UserRoleGuard。 - 未进入其他 R01 工作。
- 测试切片使用真实
SecurityFilterChain+ 真实 Accessor Bean(@SpringBootTest),未使用「允许缺失依赖 + new 默认实例」的绕过方式。 - 全仓剩余身份/requestId 解析数量已如实报告,未声称全仓完成。
R01-A2第二批请求身份与requestId收束完成,待独立代码复验。