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独立代码复验通过,可以进入精确暂存、本地提交与推送检查点。
未修改生产代码、未修改测试(除新增独立测试)、未暂存、未提交、未推送。