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

11 KiB

R01-A2 独立代码复验报告(第二批:8 个 Controller 请求身份与 requestId 收束)

日期:2026-09-17 分支:refactor/r01-structure-performance HEAD:52c8026b2e56dae7dbfae6a828a9efe23e180445(复验全程未变) 复验对象:docs/r01-a2-request-context-migration-report.md 性质:仅复验 —— 未修改生产代码、未暂存、未提交、未推送


0. 结论

R01-A2独立代码复验通过,可以进入精确暂存、本地提交与推送检查点。

FAIL = 0,SKIP = 0。 所有 FAIL 判据逐条核对后均未触发。


1. 基线

检查项 结果
分支 refactor/r01-structure-performance ✔
HEAD 52c8026b2e56dae7dbfae6a828a9efe23e180445 ✔
暂存区 空 ✔
实际差异文件 8 个 Controller + docs/backend-development-roadmap.md,加上 GlobalExceptionHandler.java(stat 缓存标记)✔
GlobalExceptionHandler.java 内容 工作区 blob 1fdc341e… == HEAD blob 1fdc341e… —— 内容零差异 ✔
git diff --check 无空白错误 ✔
历史未跟踪文件 未操作,240 个完整保留 ✔

2. 静态审查

2.1 禁用模式命中

Controller 命中
ApplicationController 0
CompanyController 0
MessagingController 0
OfferController 1 —— :210 Long.valueOf(part),业务参数解析(路径/查询参数),非身份解析
InterviewController 0
CandidateScoringController 0
CandidateScoreCardController 0
CompanyScoringDashboardController 0

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

私有身份/requestId 方法残留:0 ✔

2.2 注入方式

8 个 Controller 均为:

private final com.mydreamjob.security.CurrentUserAccessor currentUserAccessor;
private final com.mydreamjob.common.logging.RequestIdAccessor requestIdAccessor;
  • 同一个 CurrentUserAccessor / RequestIdAccessor(全限定名一致)✔
  • 无 @Autowired(required=false)、@Nullable、ObjectProvider、Optional<…>、new CurrentUserAccessor()、new RequestIdAccessor() 回退 ✔ (grep 命中的 required = false 全部是 @RequestParam,与依赖注入无关)

2.3 调用顺序与契约

核查项 结论
删除行中是否含角色/归属检查 否(assertCompany*/assertStudent*/FORBIDDEN/findCompanyByOwnerUserId/getRoles 均未被删除)✔
新增行中是否引入角色/归属判断 否 ✔
Service 调用参数 未变(仍为 userId、req、AuditContext.from(request) 等)✔
响应结构 未变(ApiResponse.ok(...))✔
DTO 未变 ✔
改动性质 仅:import 增删、字段新增、resolveUserId(…) → currentUserAccessor.requireUserId()、getRequestId(…) → requestIdAccessor.get(request)、私有方法删除 ✔

3. 独立边界测试

新增两个独立测试类(不复用实施者的测试与断言):A2IndependentVerificationTest(11 例)、A2AccessorWiringProbeTest(3 例)。

3.1 身份

覆盖 结果
8 端点 × 8 非法主体(anonymousUser、空串、空白、not-a-number、abc123、20 位溢出 12345678901234567890、0、-1) 全部 401,无 500 ✔
匿名 AnonymousAuthenticationToken、null 认证 401 ✔
合法正数主体 "7" 不被身份层判为未登录(非 401)✔
响应体检查方式 解析 JSON 后只检查 code/message,未对整响应做数字子串搜索 ✔
不回显异常类/堆栈/principal message 精确属于 {authentication required, not authenticated};无 Exception/java./principal;无 stackTrace/trace/principal 字段 ✔

3.2 角色与归属

覆盖 结果
STUDENT 访问企业操作(GET /company/candidates) 403 ✔(既有 ApplicationControllerTest.list_student_shouldReturn403 亦通过)
COMPANY 访问学生操作 企业账号在测试夹具下具备复合角色,端到端 403 由 Service 层产生;本次以既有测试套件(*Application*Test 等 630/630 全绿)作为该路径的回归证据 ✔
无 token 8 端点全部 401(filter chain 层)✔
身份失败不被角色层覆盖 非法主体访问学生/企业端点均保持 401(非 403)✔
资源不存在 既有 404 映射未改动(resolveHttpStatus 与 NOT_FOUND 注册表未变)✔
合法用户可达 Service A2AccessorWiringProbeTest#legalPrincipalReachesService 断言 messagingService.listConversations(42L) 被调用 ✔

3.3 requestId(Filter / Accessor 直接测试)

覆盖 结果
合法值原样保留 ✔
空白 / 空串 / null 重新生成 ✔
超长(100 字符)规范化为 32 ✔
CR / LF / CRLF 不进入结果 ✔(doesNotContain("\r")、doesNotContain("\n"))
非法字符(<script>、空格、分号、非 ASCII、制表符)整体拒绝 ✔
request attribute 优先于原始 header ✔(属性 attr-value 覆盖 header header-value)
响应头与响应体 requestId 相同 ✔(5 种输入 × 8 端点,已认证路径)
注入探针证明扫描器非空转 ✔(见 §5)

说明:RequestIdFilter.normalizeRequestId 为包私有,独立测试通过 public 的 RequestIdAccessor 输入原始头值来观察同一规范化规则 —— 既避开包可见性,又更贴近真实调用路径。


4. 测试强度审查

审查项 结论
satisfiesAnyOf 是否只允许两条明确文案 是 —— 仅 contains("authentication required") 与 contains("not authenticated");且该断言与 status==401、json.code==401、无异常类名等断言并列,不存在"接受任意响应"的可能 ✔
(独立测试进一步收紧为 isIn(…) 精确相等)
principal 泄漏检查是否覆盖非数字标记与溢出主体 是 —— 独立测试覆盖 not-a-number、abc123、20 位溢出主体,并对溢出主体只检查 message 字段,避免整响应数字搜索误判 ✔
/offers 是否确由 OfferController 处理并触发预期身份链 是 —— GET /offers 为 OfferController 的 @GetMapping(:53);接线探针断言该端点返回 accessor mock 注入的 requestId,证明其身份链经过 CurrentUserAccessor ✔
测试有效性(变异检测) 有效 —— A2AccessorWiringProbeTest 以 @MockBean 替换两个 Accessor:① mock 抛 InsufficientAuthenticationException 时八端点必须 401(若 Controller 未接线则不可能);② mock 返回值必须出现在响应体;③ verify(messagingService).listConversations(42L)。删除 Accessor 调用会导致该测试失败,故不是空转测试 ✔

5. 真实 HTTP 独立验证

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

项 结果
4 类非法请求(无 token / 畸形 Bearer / 非 JWT 串 / 空 Bearer)× 8 端点 全部 401 ✔
上述请求前后 22 张表逐字段比较 全部零变化 ✔
异常/堆栈/敏感信息泄漏 无 ✔
已认证路径 requestId 同源 5 种输入(合法/空白/超长/非法字符/含空格)× 8 端点,响应头 == 响应体 ✔
扫描器有效性探针 向 audit_logs 插入探针 → 命中 1 → 清理后 0;正则探针命中 —— 扫描器非空转 ✔

一处需要澄清的既有现象(非本次引入):未认证请求(无 token)的 401 由 Spring Security filter chain 直接产生,RequestIdFilter(默认 order)排在其后(Security 为 -100),因此该 401 响应不含 X-Request-Id 头、body 也无 requestId({"code":401,"message":"not authenticated","timestamp":""})。

  • 本次改动未触及任何 Filter / Security 配置(git diff --name-only 对 config/、common/logging/、security/ 为空)✔
  • 该行为在迁移前后完全一致 ✔
  • 到达 Controller 的已认证请求,其响应头与响应体 requestId 同源 ✔

此现象属既有架构事实,不构成本次复验的 FAIL 项,但如实记录以备后续单独评估。


6. 回归结果

命令 结果 要求
mvn -Dtest="RequestContextMigrationIntegrationTest,A2IndependentVerificationTest,A2AccessorWiringProbeTest,CurrentUserAccessorTest,RequestIdAccessorTest,RequestIdFilterTest" test 61 / 61,F=0 E=0 S=0 ✔
mvn -Dtest="*Application*Test,*Company*Test,*Messaging*Test,*Offer*Test,*Interview*Test,*CandidateScoring*Test,*Dashboard*Test,*RequestContext*Test,*RequestId*Test" test 630 / 630,F=0 E=0 S=0,BUILD SUCCESS ✔
mvn clean verify 2199 / 2199,Failures=0、Errors=0、Skipped=0,BUILD SUCCESS ≥ 2185 ✔
python docs\verify_openapi.py 176 paths / 214 operations,All checks passed 176/214 ✔
docs/openapi-rc.json 零修改 ✔ ✔

较实施者报告的 2185 增加 14 个测试,全部为本次独立复验新增的两个测试类。


7. FAIL 判据逐条核对

判据 是否触发
任一 Controller 仍直接解析主体或 requestId 否(7 个命中 0;OfferController 唯一命中为业务参数解析)
非法主体出现 500 否(8 端点 × 8 非法主体全部 401)
角色/归属错误码漂移 否(角色/归属检查未删未增;既有 630 项回归全绿)
requestId 可携带 CR/LF 或超长原值 否(CR/LF 被整体拒绝;超长收敛至 32;响应头/体均不含原值)
测试通过但未真正经过目标 Controller 否(接线变异检测证明必经 Accessor)
核心测试出现 SKIP 否(Skipped=0)

8. 复验过程中的自暴露问题(非产品缺陷)

新增独立测试首轮运行有 4 项失败,全部为我的测试缺陷,已修正:

# 问题 处置
1 RequestIdFilter.normalizeRequestId 为包私有,从 r01a2 包无法调用(编译错误) 改经 public 的 RequestIdAccessor 验证同一规则
2 断言"响应头来自 Accessor mock" —— 但响应头由 RequestIdFilter 设置,非 Controller 职责 改为断言响应体 requestId 来自 Accessor
3 用 PUT /companies/me 验证学生 403 —— 该角色检查在 Service 层、被 @MockBean 跳过,返回 200 改为验证"身份失败不被角色层覆盖"(401 保持)
4 含 CR/LF 的请求头在 MockMvc 客户端层即被拒绝,请求发不出 改用可承载但会被规范化规则拒绝的字符集

9. 结论

R01-A2独立代码复验通过,可以进入精确暂存、本地提交与推送检查点。

未修改生产代码、未修改测试(除新增独立测试)、未暂存、未提交、未推送。