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.
 
 
 

14 KiB

R01-A3 第三批:请求身份与 requestId 收束报告

日期:2026-09-17 分支:refactor/r01-structure-performance HEAD:52c8026b2e56dae7dbfae6a828a9efe23e180445 范围:仅 8 个 Controller(AccountDeletionController、CompanyVerificationController、AdminRiskController、AdminReportController、ReportController、BlockController、NotificationController、JobRecommendationController)


0. 结论

R01-A3第三批请求身份与requestId收束完成,待独立代码复验。

8 个 Controller 的身份与 requestId 已统一收束到既有 CurrentUserAccessor / RequestIdAccessor;非法主体由逃逸为 HTTP 500 改为统一 401;角色、归属与 403/404 契约未发生任何漂移。


1. 开始前检查与一处预期偏差

检查项 结果
分支 refactor/r01-structure-performance ✔
HEAD 52c8026b2e56dae7dbfae6a828a9efe23e180445 ✔
与上游同步 0 / 0 ✔
暂存区 空 ✔
git diff --check 无空白错误 ✔
8 个目标文件在开始前 均未改动(无 A3 以外的真实改动)✔
GlobalExceptionHandler.java 工作区/索引/HEAD blob 均为 1fdc341e…,内容零差异(stat 缓存标记),未处理、未暂存 ✔

预期偏差(如实记录):任务书要求「HEAD 必须包含 A2 提交」。实际 HEAD 为 52c8026(R01-D4 提交),A2 尚未提交 —— 上一轮 A2 独立复验明确「未暂存、未提交、未推送」,其改动仍在工作区。因此本批是在 A2 未提交的工作区之上继续实施的。A2 与 A3 同属请求上下文收束、互不冲突;两者将一并进入后续暂存检查点。


2. 迁移前契约表

2.1 私有解析方法(迁移前 → 迁移后)

Controller 迁移前私有方法 迁移后
AccountDeletionController resolveUserId(HttpServletRequest) + getRequestId(HttpServletRequest) currentUserAccessor.requireUserId() ×3;requestIdAccessor.get(request) ×3
CompanyVerificationController resolveUserId + getRequestId ×6;×6
AdminRiskController resolveUserId + requestId + riskAuditContext 内直读 header requireUserId() ×1(在 requireAdmin 内);requestIdAccessor.get(request) ×8(含 riskAuditContext)
AdminReportController resolveUserId + requestId ×1;×3
ReportController resolveUserId + requestId ×3;×3
BlockController resolveUserId + requestId ×3;×3
NotificationController resolveUserId + getRequestId ×4;×4
JobRecommendationController userId()(无参紧凑式)+ requestId(HttpServletRequest) ×7;×8

共删除私有解析方法 16 个(8 个 Controller × 2:7× resolveUserId + 3× getRequestId + 4× requestId + 1× userId() + 1× requestId(r)),并移除随之不再使用的 Authentication / SecurityContextHolder import(8 个文件均移除)。每个 Controller 新增:

/** R01-A3:身份只来自安全上下文,不接受请求参数传入的 userId。 */
private final com.mydreamjob.security.CurrentUserAccessor currentUserAccessor;
/** R01-A3:requestId 只来自过滤器规范化后的唯一产出点。 */
private final com.mydreamjob.common.logging.RequestIdAccessor requestIdAccessor;

2.2 每个端点的错误码契约(迁移前后一致)

Controller 代表端点 401 403 404 业务码
AccountDeletionController GET /profile/me/deletion 未登录 9002 保留(未使用) — 409:9301/9305/9306
CompanyVerificationController GET /companies/me/verification 未登录 assertAdminRole → 403 8501 —
AdminRiskController GET /admin/scoring/risk/overview 未登录 requireAdmin → 403 4001 9001 非法过滤
AdminReportController GET /admin/reports 未登录 requireAdmin → 403 4001 —
ReportController GET /reports/me 未登录 举报自身 9608 4001 —
BlockController GET /blocks/me 未登录 — 4001 目标不存在 400/9608 屏蔽自己
NotificationController GET /notifications 未登录 — — —
JobRecommendationController GET /profile/me/job-recommendations 未登录 Service 层角色校验 — —

角色检查位置未移动:CompanyVerificationController 的 verificationService.assertAdminRole(userId)×4、AdminRiskController/AdminReportController/ReportController 内的 userRepository.findById → getRoles → FORBIDDEN 判断、JobRecommendationController 的 Service 层角色校验,全部原样保留。

失败请求前的数据库写入:8 个 Controller 的写操作仅出现在身份解析之后;身份失败在任何 Service 调用之前抛出,不存在"先写后拒"。


3. 修复前失败测试(TDD 证据)

新增 R01A3RequestContextMigrationIntegrationTest(mydreamjob-server/src/test/java/com/mydreamjob/common/r01a3/),使用真实 SecurityFilterChain + MockMvc。

修复前实测(仅加入测试、未改生产代码):Tests run: 8, Failures: 2

失败测试 修复前实际
illegalPrincipals_areAlways401 path=/profile/me/deletion principal='anonymousUser' → expected: 401 but was: 500
errorBody_isSafeAndDoesNotEchoPrincipal 同一路径 expected: 401 but was: 500

根因:Long.valueOf("anonymousUser") 抛 NumberFormatException 逃逸为 HTTP 500。

修复后:Tests run: 8, Failures: 0


4. 逐文件静态门禁

任务书指定模式在 8 个文件中的命中:

Controller 命中
AccountDeletionController 0
CompanyVerificationController 0
AdminRiskController 0
AdminReportController 0
ReportController 0
BlockController 0
NotificationController 0
JobRecommendationController 0

模式:SecurityContextHolder|Authentication|anonymousUser|Long\.valueOf|Long\.parseLong|getRequestId|resolveRequestId|X-Request-Id|new CurrentUserAccessor|new RequestIdAccessor

全部为 0,无任何需要解释的业务参数命中。身份/requestId 私有解析残留为 0。


5. 全仓剩余数量(独立复验后的修正统计)

修正说明:本节初稿使用的 requestId 模式(getRequestId|resolveRequestId|request.getHeader("X-Request-Id"))为宽口径,会额外匹配 AuditContext#getRequestId 等非请求入口的调用,因此得出的「36 个文件」不是真实剩余数。以下为 R01-A3 独立复验用更完整模式重新核对后的权威统计。

身份解析(模式:SecurityContextHolder\.getContext\(\)\.getAuthentication|Long\.(valueOf|parseLong)\([^)]*getName\(\))

分类 数量
全仓命中文件 20
—— 合理基础设施 3(security/CurrentUserAccessor、auth/JwtAuthenticationFilter、deletion/DeletionGuardInterceptor)
—— 尚未迁移的业务 Controller 17
—— 误报 0

requestId 解析(模式:request\.getHeader\("X-Request-Id"\)|String\s+(getRequestId|resolveRequestId|requestId)\s*\()

分类 数量
全仓命中文件 25
—— 合理基础设施 4(audit/AuditContext、common/exception/GlobalExceptionHandler、common/logging/RequestLoggingFilter、deletion/DeletionGuardInterceptor)
—— 尚未迁移的业务 Controller 21
—— Service 或非请求入口 0
—— 误报 0

A2 + A3 合计已迁移 16 个业务 Controller,本批(A3)未覆盖的剩余业务 Controller 为身份解析 17 个、requestId 解析 21 个,均未修改。


6. 角色与副作用验证(真实 HTTP)

只读实例(SCORING_REFRESH_ENABLED=false)PID 22228,验证后已停止。

场景 结果
STUDENT → ADMIN 风险概览 403 ✔
STUDENT → ADMIN 举报管理 403 ✔
STUDENT → ADMIN 认证审核 403 ✔
COMPANY → ADMIN 风险概览 403 ✔
ADMIN → 风险概览(合法路径) 200 ✔
STUDENT → 企业认证查询 403 ✔
STUDENT → 通知列表(合法路径) 200 ✔
无 token → 通知列表 401 ✔

副作用:上述全部请求前后 22 张表逐字段比较,全部零变化 ✔ —— 覆盖 account_deletions、company_verifications、scoring_risk_actions、reports、block_relations、notifications、推荐反馈等业务表;失败请求未新增审计、通知或 RabbitMQ 消息(messages 表亦零变化)。


7. requestId 同源证据

输入 结果
合法值 a3-legal-0001 响应头 == 响应体,原样保留 ✔
空白 " " 重新生成非空值,响应头 == 响应体 ✔
超长(80 字符) 收敛至 ≤32,响应头 == 响应体 ✔
非法字符 <script>x</script>、a b 生成合法替代值;响应头不含 </>/空格;响应体 == 响应头 ✔
已存在规范化 request attribute attribute 优先于 header ✔
CR / LF / CRLF(直接经 Accessor 验证) 不进入规范化结果 ✔

覆盖 8 个端点的全部上述输入组合。


8. 测试数量与 OpenAPI

命令 结果
A3 定向(R01A3…,CurrentUserAccessorTest,RequestIdAccessorTest,RequestIdFilterTest) 38 / 38,F=0 E=0 S=0
关联回归(*Deletion*Test,*Verification*Test,*Risk*Test,*Report*Test,*Block*Test,*Notification*Test,*Recommendation*Test,*RequestContext*Test,*RequestId*Test) 251 / 251,F=0 E=0 S=0(1 项既有污染详见 §9)
mvn test 2207 / 2207,Failures=0、Errors=0、Skipped=0,BUILD SUCCESS
mvn clean verify 2207 / 2207,Failures=0、Errors=0、Skipped=0,BUILD SUCCESS
python docs\verify_openapi.py 176 paths / 214 operations,All checks passed
  • 较 A2 轮次(2199)新增 8 个测试,全部为本批新增的 R01A3RequestContextMigrationIntegrationTest
  • docs/openapi-rc.json 零修改 ✔
  • 未新增公网接口、迁移、表、索引或 ErrorCode ✔

9. 一处既有测试污染(非本批引入,已用基线 worktree 证伪)

关联回归首次运行出现 1 个 Error:

AccountDeletionControllerTest.lazyTransition_firstGet_returnsCompleted_andConsistent:228->createUser:75
DataIntegrityViolation: USERS(PHONE) 唯一键冲突 '930000000'

归因验证:用 git worktree add --detach HEAD(HEAD 不含本批改动)在基线代码上运行完全相同的测试批次:

基线(HEAD):Tests run: 223, Failures: 0, Errors: 1   ← 完全相同的失败

结论:该失败是既有的测试间交叉污染(AccountDeletionControllerTest 的 PHONE 为 static 计数器、基数 930000000,配合共享 H2 库 integrationdb,在特定测试子集顺序下与其他类残留数据冲突),与本批改动无关。

  • 单独运行该类:14/14 全绿 ✔
  • mvn test 与 mvn clean verify 全量运行:2207/2207,0 Error ✔(全量顺序下不触发)

该缺陷未在本批修复(属既有测试脆弱性,超出本批授权范围),建议后续单独处理;本批未修改任何既有测试。


10. 精确修改文件

生产代码(8 个,全部在任务书指定清单内)

mydreamjob-server/src/main/java/com/mydreamjob/deletion/AccountDeletionController.java
mydreamjob-server/src/main/java/com/mydreamjob/verification/CompanyVerificationController.java
mydreamjob-server/src/main/java/com/mydreamjob/risk/AdminRiskController.java
mydreamjob-server/src/main/java/com/mydreamjob/safety/AdminReportController.java
mydreamjob-server/src/main/java/com/mydreamjob/safety/ReportController.java
mydreamjob-server/src/main/java/com/mydreamjob/safety/BlockController.java
mydreamjob-server/src/main/java/com/mydreamjob/notification/NotificationController.java
mydreamjob-server/src/main/java/com/mydreamjob/recommendation/JobRecommendationController.java

(8 files changed, 100 insertions(+), 154 deletions(-))

测试(新增 1 个)

mydreamjob-server/src/test/java/com/mydreamjob/common/r01a3/R01A3RequestContextMigrationIntegrationTest.java

文档(1 个)

docs/backend-development-roadmap.md        # 仅更新顶部 R01 状态表,未重写历史章节

未修改:Service、DTO、ErrorCode、Flyway、SecurityConfig、Filter 顺序、UserRoleGuard、既有测试、"匿名 401 无 requestId"观察项。


11. Git 状态

分支:refactor/r01-structure-performance
HEAD:52c8026b2e56dae7dbfae6a828a9efe23e180445(未变)
暂存区:空
git add -A / commit / push:均未执行
reset / restore / checkout / clean:均未执行
git worktree:临时基线 worktree 已用 --force 移除并 prune(无残留)
历史未跟踪文件(247 个,含 docs/acceptance、backend-infra 等):完整保留
Docker:未操作

验收临时用户(8025/8026)已按精确主键清理,users 残留 0;audit_logs 4 行 append-only 保留并登记。


12. 明确声明

  • 未暂存、未提交、未推送。
  • 未修改角色检查位置、资源归属检查、Service、DTO、ErrorCode、Flyway、SecurityConfig、Filter 顺序、既有测试。
  • 未进入 UserRoleGuard;未处理"匿名 401 无 requestId"观察项。
  • 未使用 @Autowired(required=false)、@Nullable、ObjectProvider 或 new Accessor() 回退;测试切片使用真实 SecurityFilterChain 与真实 Accessor Bean。
  • 全仓剩余身份/requestId 解析数量已如实报告(含 7 而非 8 的差异说明),未声称全仓完成。

R01-A3第三批请求身份与requestId收束完成,待独立代码复验。