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

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收束完成,待独立代码复验。