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