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

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