14 KiB
R01-A4 第四批:请求身份与 requestId 收束报告
日期:2026-09-17
分支:refactor/r01-structure-performance
HEAD:35baa8416d31e71dd2c2556877f9d2e1c93b14a1
范围:仅 8 个 Controller(CandidateFeedbackController、ApplicationMatchController、MatchConversationController、MatchRuleController、TalentDiscoveryController、CareerProgressController、MatchController、TalentInvitationController)
0. 结论
R01-A4第四批请求身份与requestId收束完成,待独立代码复验。
8 个 Controller 的身份与 requestId 已统一收束到既有 CurrentUserAccessor / RequestIdAccessor;非法主体由逃逸为 HTTP 500 改为统一 401;角色、归属与 403/404 契约未发生漂移。
1. 基线
| 检查项 | 结果 |
|---|---|
| 分支 | refactor/r01-structure-performance ✔ |
| HEAD | 35baa8416d31e71dd2c2556877f9d2e1c93b14a1 ✔ |
| 与上游同步 | 0 / 0 ✔ |
| 暂存区 | 空 ✔ |
git diff --check |
无空白错误 ✔ |
| 8 个目标文件开始前状态 | 均未改动(无本批以外修改)✔ |
GlobalExceptionHandler.java |
仍为 stat 缓存标记(内容零差异),未处理、未暂存 ✔ |
| 未跟踪文件 | 247 个,全部保护 ✔ |
2. 迁移前契约表
| Controller | 真实 Mapping(代表端点) | 允许角色 / 归属 | 私有解析方法 | 写操作涉及表 |
|---|---|---|---|---|
| CandidateFeedbackController | POST/GET/PUT/DELETE /applications/{applicationId}/feedback |
企业(assertCompanyOwner)+ 学生可读 |
resolveUserId + getRequestId |
candidate_feedback |
| ApplicationMatchController | PUT /applications/{id}/candidate-interest-score、PUT .../hr-suitability-score、GET /applications/{id}/match |
学生 / 企业(Service 层) | 同上 | application_match_records、application_match_events |
| MatchConversationController | POST /applications/{id}/conversation |
匹配双方 | 同上 | conversations、conversation_members |
| MatchRuleController | GET/PUT /companies/me/match-rule、GET/PUT /profile/me/match-rule |
企业 / 学生各自规则 | 同上 | company_match_rules、student_match_rules |
| TalentDiscoveryController | GET/PUT /profile/me/talent-discovery、GET /companies/me/jobs/{jobId}/talent-recommendations |
学生(偏好)/ COMPANY(推荐) | userId() + requestId()(紧凑式) |
talent_discovery_preferences |
| CareerProgressController | GET /profile/me/career-progress |
STUDENT | resolveUserId + getRequestId |
只读 |
| MatchController | GET /jobs/{jobId}/match |
STUDENT | 同上 | 只读 |
| TalentInvitationController | POST /companies/me/jobs/{jobId}/talent-invitations、GET /companies/me/talent-invitations、GET /profile/me/talent-invitations、POST /profile/me/talent-invitations/{id}/decision 等 6 个 |
企业创建 / 学生决定 / 双方读取 | userId() + requestId()(紧凑式) |
talent_invitations |
契约要点:8 个 Controller 的角色与资源归属判断全部位于 Service 层(Controller 仅解析身份后委派),因此本批迁移不涉及任何角色判断的移动。
迁移前状态码:非法/匿名主体因 Long.valueOf(auth.getName()) 抛 NumberFormatException → HTTP 500(见 §3 实测)。
3. 修复前失败测试(TDD 证据)
新增 R01A4RequestContextMigrationIntegrationTest(mydreamjob-server/src/test/java/com/mydreamjob/common/r01a4/),真实 SecurityFilterChain + MockMvc。
修复前实测(仅加入测试、未改生产代码):Tests run: 9, Failures: 3
| 失败测试 | 修复前实际 |
|---|---|
illegalPrincipals_areAlways401 |
path=/applications/1/feedback principal='anonymousUser' → expected: 401 but was: 500 |
conversationEndpoint_illegalPrincipals_are401 |
principal='anonymousUser' → expected: 401 but was: 500 |
errorBody_isSafe |
同路径 → 500 |
修复后:Tests run: 9, Failures: 0
4. 生产修改
8 个 Controller 统一构造器注入(均为 private final):
/** R01-A4:身份只来自安全上下文,不接受请求参数传入的 userId。 */
private final com.mydreamjob.security.CurrentUserAccessor currentUserAccessor;
/** R01-A4:requestId 只来自过滤器规范化后的唯一产出点。 */
private final com.mydreamjob.common.logging.RequestIdAccessor requestIdAccessor;
| Controller | requireUserId() |
requestIdAccessor.get() |
|---|---|---|
| CandidateFeedbackController | 4 | 4 |
| ApplicationMatchController | 3 | 3 |
| MatchConversationController | 1 | 1 |
| MatchRuleController | 4 | 4 |
| TalentDiscoveryController | 3 | 3 |
| CareerProgressController | 1 | 1 |
| MatchController | 1 | 1 |
| TalentInvitationController | 6 | 6 |
共删除私有解析方法 16 个(8 Controller × 2:6× resolveUserId + 6× getRequestId + 2× userId() + 2× requestId()),并移除随之不再使用的 Authentication / SecurityContextHolder import。
未做:未使用可选注入 / @Nullable / ObjectProvider / new Accessor();未新增 userId 请求参数;未移动或删除角色检查与资源归属检查;未修改 Service 调用顺序、错误码或接口结构;未顺手重构紧凑代码。
5. 静态门禁
任务书指定模式在 8 个文件中的命中:
| Controller | 命中 |
|---|---|
| CandidateFeedbackController | 0 |
| ApplicationMatchController | 0 |
| MatchConversationController | 0 |
| MatchRuleController | 0 |
| TalentDiscoveryController | 0 |
| CareerProgressController | 0 |
| MatchController | 0 |
| TalentInvitationController | 0 |
全部为 0,无业务参数解析误报。身份/requestId 私有解析残留为 0。
全仓剩余统计
身份解析(SecurityContextHolder.getContext().getAuthentication | Long.(valueOf|parseLong)([^)]*getName())
| A3 后 | A4 后 | 预期 | |
|---|---|---|---|
| 全仓文件 | 20 | 12 | 12 ✔ |
| —— 基础设施 | 3 | 3(security/CurrentUserAccessor、auth/JwtAuthenticationFilter、deletion/DeletionGuardInterceptor) |
— |
| —— 业务 Controller | 17 | 9 | 9 ✔ |
| —— 误报 | 0 | 0 | — |
requestId 解析(request.getHeader("X-Request-Id") | request.getHeader(RequestIdFilter.REQUEST_ID_HEADER) | String (getRequestId|resolveRequestId|requestId)()
| A3 后 | A4 后 | 预期 | |
|---|---|---|---|
| 全仓文件(本轮完整模式) | 25 | 18 | 17 |
全仓文件(A3 口径,不含 REQUEST_ID_HEADER 项) |
25 | 17 | 17 ✔ |
| —— 基础设施(完整模式) | 4 | 5 | — |
| —— 业务 Controller | 21 | 13 | 13 ✔ |
| —— Service / 非请求入口 | 0 | 0 | — |
| —— 误报 | 0 | 0 | — |
数量差异的逐文件解释(不修改代码迁就数字):本轮模式比 A3 口径多含 request.getHeader(RequestIdFilter.REQUEST_ID_HEADER) 一项,该模式额外命中的唯一文件是 common/logging/RequestIdAccessor.java(第 35 行的兜底读取 request.getHeader(RequestIdFilter.REQUEST_ID_HEADER))—— 这是 Accessor 自身的合法实现,属基础设施,非业务入口。因此:完整模式 18 = 基础设施 5 + 业务 13;A3 口径 17 = 基础设施 4 + 业务 13。业务 Controller 数量与预期完全一致(13)。
6. 角色与副作用验证
6.1 既有测试回归(角色/归属语义证据)
| 命令 | 结果 |
|---|---|
A4 关联回归(*CandidateFeedback*Test,*ApplicationMatch*Test,*MatchConversation*Test,*MatchRule*Test,*TalentDiscovery*Test,*CareerProgress*Test,*MatchController*Test,*TalentInvitation*Test,*RequestContext*Test,*RequestId*Test) |
163 / 163,F=0 E=0 S=0,BUILD SUCCESS |
既有测试覆盖:反馈仅企业可写、匹配双方可见性、会话归属、匹配规则角色、人才发现 COMPANY 权限、职业进度 STUDENT 权限、邀约企业创建/学生决定/双方读取、其他企业与其他学生的 403/404。
6.2 真实 HTTP 验证
只读实例 PID 7648,验证后已停止。
| 场景 | 结果 |
|---|---|
| 无 token / 畸形 Bearer / 纯数字 Bearer × 7 端点 | 全部 401 ✔ |
STUDENT → GET /companies/me/match-rule |
403 ✔ |
COMPANY → GET /companies/me/match-rule |
403(该账号无企业资料,既有权属语义)✔ |
STUDENT → GET /profile/me/career-progress |
200 ✔ |
COMPANY → GET /profile/me/career-progress |
200(该账号注册时自带 STUDENT 角色,属既有语义)✔ |
STUDENT → GET /profile/me/talent-discovery |
200 ✔ |
ADMIN → GET /profile/me/talent-discovery |
403 ✔ |
STUDENT → GET /profile/me/talent-invitations |
200 ✔ |
COMPANY → GET /companies/me/talent-invitations |
403(无企业资料)✔ |
6.3 副作用快照
上述全部请求前后 22 张表逐行稳定排序的逐字段快照比对 → 全部零变化 ✔
- 覆盖表含
candidate_feedback、application_match_records、application_match_events、conversations、conversation_members、company_match_rules、student_match_rules、talent_discovery_preferences、talent_invitations、notifications、messages、audit_logs等 - 未使用 COUNT/SUM 代替内容快照;采用显式列 + 稳定
ORDER BY的逐字段比较
7. 独立验证辅助测试(实施证据)
新增 A4AccessorWiringProbeTest(4 例),证明接线真实存在:
| 用例 | 断言 |
|---|---|
accessorThrowsUnauthenticated_endpointsReturn401 |
mock Accessor 抛认证异常 → 7 端点全部 401(若未接线则不可能) |
serviceReceivesAccessorUserId |
mock 返回 4242L → careerProgressService.getCareerProgress(4242L, …)、talentInvitationService.listCompany(4242L, …) 收到该值 |
requestIdComesFromAccessorBean |
mock 的 requestId 必须出现在响应体 |
noToken_is401 |
无 Token → Security 链 401 |
删除 Accessor 调用会使本测试失败 ✔(属实施证据,不代替后续独立复验)
8. requestId 同源证据
| 输入 | 结果 |
|---|---|
合法值 a4-legal-0001 |
响应头 == 响应体 requestId,原样保留 ✔ |
空白 " " |
重新生成非空值,头体同源 ✔ |
| 超长(80 字符) | 收敛至 ≤32,头体同源 ✔ |
非法字符 <script>x</script>、a b |
生成合法替代值;响应头不含 </>/空格 ✔ |
| request attribute 与 header 不同 | attribute 优先 ✔ |
| CR / LF / CRLF(直接经 Accessor) | 不进入规范化结果 ✔ |
覆盖 8 个端点(含 POST 端点)。
9. 测试数量与 OpenAPI
| 命令 | 结果 |
|---|---|
A4 定向(R01A4…,A4AccessorWiringProbeTest,CurrentUserAccessorTest,RequestIdAccessorTest,RequestIdFilterTest) |
43 / 43,F=0 E=0 S=0 |
| A4 关联回归 | 163 / 163,F=0 E=0 S=0 |
mvn test |
2238 / 2238,Failures=0、Errors=0、Skipped=0 |
mvn clean verify |
2238 / 2238,Failures=0、Errors=0、Skipped=0,BUILD SUCCESS(≥ 2225 ✔) |
python docs\verify_openapi.py |
176 paths / 214 operations,All checks passed |
- 较上轮(2225)新增 13 个测试(9 + 4)
docs/openapi-rc.json零修改 ✔- V1–V45 零修改 ✔
- 未新增接口、表、索引、迁移或 ErrorCode ✔
- 未发现新的基线测试污染(上一轮的账号注销夹具隔离修复已生效)✔
10. 精确修改文件
生产代码(8 个,全部在任务书指定清单内)
mydreamjob-server/src/main/java/com/mydreamjob/feedback/CandidateFeedbackController.java
mydreamjob-server/src/main/java/com/mydreamjob/bidirectionalmatch/ApplicationMatchController.java
mydreamjob-server/src/main/java/com/mydreamjob/bidirectionalmatch/MatchConversationController.java
mydreamjob-server/src/main/java/com/mydreamjob/bidirectionalmatch/MatchRuleController.java
mydreamjob-server/src/main/java/com/mydreamjob/talentdiscovery/TalentDiscoveryController.java
mydreamjob-server/src/main/java/com/mydreamjob/careerprogress/CareerProgressController.java
mydreamjob-server/src/main/java/com/mydreamjob/match/MatchController.java
mydreamjob-server/src/main/java/com/mydreamjob/talentinvitation/TalentInvitationController.java
测试(新增 2 个)
mydreamjob-server/src/test/java/com/mydreamjob/common/r01a4/R01A4RequestContextMigrationIntegrationTest.java
mydreamjob-server/src/test/java/com/mydreamjob/common/r01a4/A4AccessorWiringProbeTest.java
文档(1 个)
docs/backend-development-roadmap.md # 仅更新顶部 R01 状态行,历史章节未动
未修改:Service、DTO、ErrorCode、Flyway、SecurityConfig、Filter 顺序、OpenAPI、既有测试、UserRoleGuard、"匿名 401 缺 requestId"观察项。
11. Git 状态
分支:refactor/r01-structure-performance
HEAD:35baa8416d31e71dd2c2556877f9d2e1c93b14a1(未变)
暂存区:空
git add -A / commit / push:均未执行
Docker:未操作
历史未跟踪文件:247 个完整保留
验收临时用户(a4vstu / a4vcom)已按精确主键清理,users 残留 0;audit_logs 4 行 append-only 保留并登记。
12. 明确声明
- 未暂存、未提交、未推送。
- 未进入
UserRoleGuard;未处理"匿名 401 缺 requestId"观察项。 - 未使用
@Autowired(required=false)、@Nullable、ObjectProvider或new Accessor()回退;测试切片使用真实SecurityFilterChain与真实 Accessor Bean。 - 全仓剩余身份/requestId 数量已逐文件分类解释,未修改代码迁就数字。
R01-A4第四批请求身份与requestId收束完成,待独立代码复验。