# 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 新增: ```java /** 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,响应头 == 响应体 ✔ | | 非法字符 ``、`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收束完成,待独立代码复验。**