Переглянути джерело

docs(openspec): add-request-auth-replay-guard (基座鉴权+防重放)

新增 Phase B.2 核心专项:mjava 基座 HMAC-SHA256 请求鉴权 + Nonce 防重放。

- proposal/design/spec/tasks 四件套 valid
- 能力:request-auth(Header: X-MJ-Key/Timestamp/Nonce/Signature)+ replay-guard(时间窗 + NonceCache LRU)
- 默认 enabled=false 保障 mcli/shunfeng/guangming 零破坏;按需开启
- 三级豁免:全局开关 / exempt-paths / @NoAuth 注解
- 签名协议:HMAC-SHA256(secret, timestamp + "\n" + nonce + "\n" + method + "\n" + path + "\n" + bodyHash)
- 密钥走 ${AUTH_SECRET} 环境变量

同步更新:
- add-mjava-com/design.md 声明本 change 为前置依赖(CallerAuthInterceptor 直接复用 UtilSignature + NonceCache)
- CLAUDE.md 加 change 条目
- BACKLOG.md 加 B2.7,标注为 mjava-com 前置依赖

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
malk 2 тижнів тому
батько
коміт
8c1df66cf9

+ 2 - 1
CLAUDE.md

@@ -25,8 +25,9 @@ Java 后端基座 + 客户子项目仓库。Spring Boot 2.2.13 + MySQL,第一
 - `changes/init-project-baseline/` — 基线沉淀,仅 `mvn compile` 验证阻塞在 Maven 未装
 - `changes/extend-yida-api-coverage/` — **Phase B.1 最高优先级**:宜搭表单+流程 API 对齐
 - `changes/extend-dingtalk-contacts-api/` — Phase B.1:钉钉通讯录 API 对齐
+- `changes/add-request-auth-replay-guard/` — Phase B.2:基座请求鉴权 + 防重放(mjava-com 的前置依赖)
 - `changes/add-mjava-pro/` — Phase C:多客户单部署(待 B 完成后)
-- `changes/add-mjava-com/` — Phase C:通用能力 BaaS(待 B 完成后)
+- `changes/add-mjava-com/` — Phase C:通用能力 BaaS(待 B 完成后,依赖 add-request-auth-replay-guard
 
 ## 快速操作
 

+ 9 - 8
openspec/BACKLOG.md

@@ -48,14 +48,15 @@
 
 不依赖 pro/com 专项的基座功能增强,都先做在 `mjava/` 基座里,让三个现存客户模块可立刻受益。
 
-| ID | 任务 | 需求出处 | 备注 |
-|----|------|---------|------|
-| B2.1 | `UtilHttp` 内部落实 §3.5 审计日志(请求/响应脱敏 + latency + success) | mjava-baseline §3.5 | 脱敏规则:token/appSecret/password/aesKey/privateKey → `***` |
-| B2.2 | `UtilToken` 支持 namespace 参数(为 mjava-pro 租户隔离铺路) | mjava-pro design.md | API:`put(namespace, key, value, ttl)`;回退模式保留无 namespace 版本 |
-| B2.3 | `CallerRateLimiter` 抽象(通用限流组件) | mjava-com design.md | Guava RateLimiter 封装;也可服务 pro 的租户级限流 |
-| B2.4 | 完成 `add-observability-foundation` tasks 4.2 / 5.2(生产冒烟) | observability change | 需 Maven |
-| B2.5 | 把 `Client` 方法兼容全参数的**代码审查清单**(checklist)沉淀进 `/opsx:propose` 的模板 | 用户规范要求 | 避免将来 PR 漏传字段 |
-| B2.6 | 请求监听的 trace 输出示例(配合 `TraceIdFilter` 展示完整链路格式) | baseline §3.5 / §8 | 文档用 |
+| ID | 任务 | 归属 change | 备注 |
+|----|------|-------------|------|
+| B2.1 | `UtilHttp` 内部落实 §3.5 审计日志(请求/响应脱敏 + latency + success) | 待立项 | 脱敏规则:token/appSecret/password/aesKey/privateKey → `***` |
+| B2.2 | `UtilToken` 支持 namespace 参数(为 mjava-pro 租户隔离铺路) | 待立项 | API:`put(namespace, key, value, ttl)` |
+| B2.3 | `CallerRateLimiter` 抽象(通用限流组件) | 待立项 | Guava RateLimiter 封装 |
+| B2.4 | 完成 `add-observability-foundation` tasks 4.2 / 5.2(生产冒烟) | observability | 需 Maven |
+| B2.5 | 把 Client 方法兼容全参数的**代码审查清单**(checklist)沉淀进 `/opsx:propose` 的模板 | 未定,文档侧 | 避免将来 PR 漏传字段 |
+| B2.6 | 请求监听的 trace 输出示例 | 文档 | baseline §3.5 / §8 |
+| **B2.7** | **基座请求鉴权 + 防重放**(HMAC-SHA256 + 时间窗 + Nonce) | `changes/add-request-auth-replay-guard/` | ✅ 规则就绪,0/33 任务;**mjava-com 的前置依赖** |
 
 ## 阶段 C — 专项子模块开发(最后启动)
 

+ 6 - 0
openspec/changes/add-mjava-com/design.md

@@ -1,3 +1,9 @@
+## 依赖声明
+
+**前置依赖**:`add-request-auth-replay-guard`(基座 `UtilSignature` / `NonceCache` 已落地)
+
+mjava-com 的 `CallerAuthInterceptor` **不重新实现** HMAC 签名与 Nonce 去重,而是直接调用基座提供的 `UtilSignature.sign()` 与 `NonceCache.putIfAbsent()`;本 change 只负责"按 callerId 动态查 secret"与"按 callerId 限流"两层。
+
 ## 架构要点
 
 ### 请求流程

+ 130 - 0
openspec/changes/add-request-auth-replay-guard/design.md

@@ -0,0 +1,130 @@
+## 签名协议(客户端必须遵守)
+
+请求头固定格式:
+
+```
+X-MJ-Key:       {调用方标识,便于审计,非秘密}
+X-MJ-Timestamp: {Unix 毫秒}
+X-MJ-Nonce:     {32 字符随机串}
+X-MJ-Signature: {HMAC-SHA256(sharedSecret, timestamp + "\n" + nonce + "\n" + method + "\n" + path + "\n" + bodyHash)}
+```
+
+其中:
+- `method`:`GET` / `POST` / ...(大写)
+- `path`:不含 host 与 query 的路径(如 `/api/mcli/personnel/sync`)
+- `bodyHash`:`SHA256(requestBody bytes)` 的 hex;GET 请求 body 为空字符串
+
+签名算法固定 HMAC-SHA256,结果以 hex 小写输出。
+
+## 请求生命周期
+
+```
+请求到达
+  ↓
+AuthFilter.doFilter
+  ├── enabled == false?  → 直接 chain.doFilter (默认行为)
+  ├── path 在 exempt-paths?  → 放行
+  ├── handler method 有 @NoAuth?  → 放行(需要 spring MVC 上下文,见下)
+  │
+  ├── 校验 Header 齐全 → 缺字段 401
+  ├── 校验 timestamp 在 ±window 内 → 超窗 401
+  ├── 校验 nonce 未用过 → NonceCache.putIfAbsent → 冲突 401(重放)
+  ├── 计算期望 signature → 比对 → 不匹配 403
+  └── 全部通过 → MDC 记录 authKey → chain.doFilter
+
+afterCompletion
+  ├── 清 MDC
+```
+
+**实现难点**:`AuthFilter` 是 `javax.servlet.Filter`,在 Spring MVC 分发前执行,无法直接拿到 `HandlerMethod` 检查 `@NoAuth`。两种解决方案:
+
+- **方案 A**(推荐):`AuthFilter` 只做 Header 齐全性与时间戳/Nonce 校验;签名校验延后到 `HandlerInterceptor.preHandle`(那时能拿到 HandlerMethod,可读 `@NoAuth`)
+- **方案 B**:`AuthFilter` 一次到位,`@NoAuth` 只支持类级别(通过 `RequestMappingHandlerMapping` 提前解析)
+
+Phase 1 选方案 A(拆分两层),让注解豁免精确到方法。
+
+## Nonce 去重
+
+`NonceCache` 基于 `UtilToken.TimedCache`:
+
+- Key = `nonce:{X-MJ-Nonce}`
+- Value = 任意占位(如 `X-MJ-Key`,便于日志)
+- TTL = `mjava.auth.window + 30s`(窗口 + 冗余 30s,避免边界时间戳造成"刚过期还被接受")
+- 容量上限 `nonce-cache-size`,LRU 淘汰
+
+Nonce 已存在 → 判定重放。
+
+## 豁免策略
+
+三种豁免方式(优先级从上到下):
+
+1. **`enabled: false`**(全局开关):完全不做校验
+2. **`exempt-paths`**(路径匹配):Ant 风格,`/actuator/**` / `/api/*/callback/**`
+3. **`@NoAuth`** 注解:方法级或类级,粒度最细
+
+## 密钥管理
+
+- Phase 1:单一共享密钥 `mjava.auth.secret`,通过环境变量 `AUTH_SECRET` 注入
+- 换密钥:两段式。先同时接受旧 + 新密钥(`secret` + `secret-next`),客户端切完后再移除旧密钥。本 change 不做换密钥能力(YAGNI),需要时再加
+- Phase 2(mjava-com):从宜搭权限表单动态加载 `callerId → secret` 映射,替换 Phase 1 的单密钥
+
+## 错误返回
+
+统一返回 `McR.fail()`,错误码:
+
+| HTTP | code | 原因 |
+|------|------|------|
+| 401 | `AUTH_HEADER_MISSING` | 必备 Header 缺失 |
+| 401 | `AUTH_TIMESTAMP_OUT_OF_WINDOW` | 时间戳超窗 |
+| 401 | `AUTH_NONCE_REPLAYED` | Nonce 已使用(重放) |
+| 403 | `AUTH_SIGNATURE_INVALID` | 签名不匹配 |
+| 503 | `AUTH_CONFIG_MISSING` | 服务端未配置 secret(`enabled: true` 但 `secret` 为空) |
+
+出于安全考虑,错误信息**不暴露**具体环节("Header 缺" 不告诉缺哪个、"签名错" 不告诉期望值),避免给攻击者定位线索。日志侧记录详情。
+
+## 配置项详细
+
+```yaml
+mjava:
+  auth:
+    enabled: false                # 开关
+    secret: ${AUTH_SECRET}        # HMAC 共享密钥
+    secret-next:                  # 灰度切换密钥(可选,本 change 先不实现)
+    window: 300                   # 秒
+    nonce-cache-size: 10000       # 条目上限
+    exempt-paths:
+      - /actuator/**
+      - /api/*/callback/**
+      - /api/*/sso/**             # 部分 SSO 回跳自带签名
+```
+
+## 兼容策略
+
+- 默认 `enabled: false`,生产服务启动后行为 = 当前现状 ✅ 零破坏
+- 客户升级时机:客户侧接入完成签名 → 打开 `enabled: true` → 灰度切换
+- 若某客户永远不开启,也不影响(功能可选)
+
+## Non-Goals(设计边界)
+
+- 不做分布式 Nonce 去重(单实例内存够用;多实例部署时再评估 Redis,见 BACKLOG 触发条件)
+- 不做密钥轮换自动化
+- 不做基于 IP 的访问控制(交给运维层 / 网关)
+- 不做 QPS 限流(`CallerRateLimiter` 是独立专项,见 mjava-com)
+
+## 与 mjava-com 的关系
+
+| 层 | 提供什么 | 谁用 |
+|----|---------|------|
+| 本 change(`request-auth` + `replay-guard`) | 通用 HMAC 签名 + 防重放基础设施 | 所有 mjava 子模块(按需启用) |
+| `add-mjava-com` | 调用方注册表(宜搭权限表单)+ 多 caller 独立密钥 + 限流 | mjava-com 网关 |
+
+mjava-com 的 `CallerAuthInterceptor` 内部将**直接调用本 change 提供的 `UtilSignature` 与 `NonceCache`**,只增加"按 callerId 动态查 secret"与"按 callerId 限流"两层。
+
+## 风险与缓解
+
+| 风险 | 缓解 |
+|------|------|
+| 默认关闭 → 用户忘记开启,裸奔 | README + CLAUDE.md 明示"生产客户建议开启" |
+| 签名计算客户端实现差异大 | 提供 Java / Python / Node.js 三份参考实现代码 |
+| 时间窗过严导致客户端时钟漂移失败 | 默认 5 分钟宽松;文档说明 NTP 同步要求 |
+| `@NoAuth` 滥用 | Code review 规定:豁免必须在 PR 描述里写明理由 |

+ 64 - 0
openspec/changes/add-request-auth-replay-guard/proposal.md

@@ -0,0 +1,64 @@
+> 状态(2026-04-18 立项):**提案阶段,未实施**
+> 优先级:高(Phase B.2 基座安全增强,早于 mjava-com 专项)
+
+## Why
+
+mjava 基座当前 API 安全裸奔:
+
+- **无统一鉴权**:任何知道 URL 的调用方都能直接访问任意 Controller(`filter/RequestFilter` 和 `RequestInterceptor` 当前只打日志,不做任何校验)
+- **无防重放保护**:即便将来加签名,不带时间窗 + Nonce 的话攻击者仍可抓包重放
+- **客户子项目各自重造**:mjava-guangming 的 SSO 校验、mjava-shunfeng 的 dingtalk webhook 校验各自实现,代码不共享
+
+需要在基座提供**可选启用**的"请求鉴权 + 防重放"统一能力。默认关闭(保护现有 mcli/shunfeng/guangming 生产服务不中断),子项目按需开启;mjava-com 未来的调用方签名也将复用本能力,避免重复建设。
+
+与 `add-observability-foundation` 的 TraceId 互补:前者负责"日志可追溯",本 change 负责"调用不可伪造"。
+
+## What Changes
+
+新增 3 个基座组件(全部放在 `mjava/`,子项目零代码改动即可通过配置启用):
+
+- `com.malk.filter.AuthFilter`:请求进入时校验 token/signature + 时间窗 + Nonce 去重;失败返回 `401/403`
+- `com.malk.utils.UtilSignature`:HMAC-SHA256 签名计算与验证工具类
+- `com.malk.core.NonceCache`:基于 `UtilToken.TimedCache` 的 Nonce 去重缓存(封装 namespace + TTL = 时间窗)
+
+配置开关:
+
+```yaml
+mjava:
+  auth:
+    enabled: false           # 默认关闭;子项目可按需打开
+    secret: ${AUTH_SECRET}   # 服务端共享密钥(环境变量注入)
+    window: 300              # 时间窗秒数,默认 5 分钟
+    nonce-cache-size: 10000  # Nonce 缓存条目上限(LRU 淘汰)
+    exempt-paths:            # 豁免路径清单(正则或 Ant 风格)
+      - /actuator/**         # 健康检查
+      - /api/*/callback/**   # 第三方 webhook 回调(自带签名协议)
+```
+
+另加 `@NoAuth` 注解,方法/类级别显式豁免。
+
+## Capabilities
+
+### New Capabilities
+- `request-auth`:基于 HMAC-SHA256 + 共享密钥的统一请求鉴权
+- `replay-guard`:基于时间窗 + Nonce 的防重放保护
+
+### Modified Capabilities
+<!-- 基座 filter 链追加,不改既有 RequestFilter/RequestInterceptor 职责 -->
+
+## Impact
+
+- **新增文件**:`AuthFilter.java` / `UtilSignature.java` / `NonceCache.java` / `AuthConfig.java` / `@NoAuth` 注解 / `AuthConfigProperties.java`
+- **不动**:`RequestFilter` / `RequestInterceptor` / `CatchException` 原职责
+- **yml**:`application.yml` 新增 `mjava.auth.*` 结构,默认 `enabled: false`
+- **对 mcli / shunfeng / guangming**:零影响(`enabled: false` 默认不拦截)
+- **对 mjava-com**:未来的调用方鉴权直接用本 `UtilSignature` + `NonceCache`;mjava-com 的 `CallerAuthInterceptor` 设计可简化
+
+## Non-Goals
+
+- ❌ 不做 OAuth2 / OpenID Connect(重量级,不适合内部场景)
+- ❌ 不做 JWT 签发与刷新流程(当前是无状态 HMAC,够用;将来如需再单独提案)
+- ❌ 不做 mTLS(网络层配置由运维侧处理)
+- ❌ 不取代第三方 webhook 回调的供应商签名校验(钉钉 `DingCallbackCrypto` / 宜搭 token 验证等各自保留)
+- ❌ 不做角色/权限(RBAC/ABAC),只解决"是谁 + 请求是否合法 + 是否重放"三件事
+- ❌ 本 change 不强制要求任何现存客户升级开启

+ 58 - 0
openspec/changes/add-request-auth-replay-guard/specs/replay-guard/spec.md

@@ -0,0 +1,58 @@
+## ADDED Requirements
+
+### Requirement: 时间窗校验
+
+mjava 基座 SHALL 拒绝时间戳超出配置窗口的请求,以限制重放攻击的有效窗口。
+
+#### Scenario: 时间戳在窗口内
+
+- **WHEN** `X-MJ-Timestamp` 与服务端当前时间差 ≤ `mjava.auth.window`(默认 300 秒)
+- **THEN** 进入后续 Nonce 与签名校验
+
+#### Scenario: 时间戳超窗
+
+- **WHEN** 时间差 > `window`(无论提前还是滞后)
+- **THEN** 返回 `401 { code: "AUTH_TIMESTAMP_OUT_OF_WINDOW" }`
+- **AND** 审计日志记录 `clientTimestamp` 与 `serverTimestamp` 供排查
+
+### Requirement: Nonce 去重
+
+mjava 基座 SHALL 通过 Nonce 缓存实现在时间窗内的一次性请求保证。同一 Nonce 在窗口期内只允许通过一次。
+
+#### Scenario: 首次 Nonce 被接受
+
+- **WHEN** 请求携带之前未见过的 `X-MJ-Nonce`
+- **THEN** `NonceCache.putIfAbsent(nonce)` 成功
+- **AND** 请求进入签名校验
+
+#### Scenario: Nonce 被重放
+
+- **WHEN** 请求携带之前已见过的 Nonce(仍在 TTL 内)
+- **THEN** `NonceCache.putIfAbsent` 返回 false
+- **AND** 返回 `401 { code: "AUTH_NONCE_REPLAYED" }`
+
+#### Scenario: Nonce 缓存容量上限
+
+- **WHEN** `NonceCache` 达到 `mjava.auth.nonce-cache-size` 上限
+- **THEN** 按 LRU 策略淘汰最旧条目
+- **AND** 淘汰不影响未淘汰 Nonce 的判定正确性
+
+### Requirement: Nonce 缓存 TTL 设置
+
+`NonceCache` 的 TTL MUST 略长于 `mjava.auth.window`,以避免边界时间戳被错误接受。
+
+#### Scenario: TTL 等于窗口 + 30s
+
+- **WHEN** NonceCache 初始化
+- **THEN** TTL = `mjava.auth.window + 30` 秒
+- **AND** 例:window=300 → TTL=330
+
+### Requirement: 单实例内存限制声明
+
+Phase 1 的 Nonce 去重 SHALL 在单 JVM 实例内生效;多实例部署时同一 Nonce 可能被两个实例各接受一次。这是已知限制,不在本 change 范围。
+
+#### Scenario: 多实例部署告警
+
+- **WHEN** 生产部署判定需要多实例横向扩展
+- **THEN** 运维必须评估是否升级为分布式 Nonce(Redis),独立走新 change 提案
+- **AND** 本 change 的 NonceCache 配置 `nonce-cache-size` 建议单实例足够承载高峰 QPS × window 秒的组合

+ 62 - 0
openspec/changes/add-request-auth-replay-guard/specs/request-auth/spec.md

@@ -0,0 +1,62 @@
+## ADDED Requirements
+
+### Requirement: HMAC 签名校验
+
+mjava 基座 SHALL 提供可配置的 HMAC-SHA256 请求签名校验机制。当 `mjava.auth.enabled=true` 时,所有非豁免请求 MUST 通过签名校验才能进入业务 Controller。
+
+#### Scenario: 合法签名请求通过
+
+- **WHEN** 请求携带完整四个 Header(`X-MJ-Key` / `X-MJ-Timestamp` / `X-MJ-Nonce` / `X-MJ-Signature`)
+- **AND** 签名 = `HMAC-SHA256(secret, timestamp + "\n" + nonce + "\n" + method + "\n" + path + "\n" + bodyHash)`
+- **THEN** 请求被放行到 Controller
+- **AND** MDC 写入 `authKey` 字段供日志追溯
+
+#### Scenario: 签名不匹配
+
+- **WHEN** 计算出的签名与 Header 中 `X-MJ-Signature` 不一致
+- **THEN** 返回 `403 { code: "AUTH_SIGNATURE_INVALID" }`
+- **AND** 响应 message 不包含期望签名值(避免泄露线索)
+- **AND** 审计日志记录 `authKey + path + latencyMs`
+
+#### Scenario: Header 缺失
+
+- **WHEN** 四个必备 Header 中任一缺失
+- **THEN** 返回 `401 { code: "AUTH_HEADER_MISSING" }`
+- **AND** 响应不说明具体缺失哪个
+
+### Requirement: 豁免机制
+
+本能力 SHALL 支持三级豁免策略,优先级从上到下:全局开关 > 路径豁免 > 注解豁免。
+
+#### Scenario: 全局关闭
+
+- **WHEN** `mjava.auth.enabled=false`
+- **THEN** 所有请求跳过鉴权,行为等同于 enable 前的裸奔状态
+
+#### Scenario: 路径豁免
+
+- **WHEN** 请求路径匹配 `mjava.auth.exempt-paths` 列表中任一 Ant 风格模式
+- **THEN** 跳过签名校验
+- **AND** 典型清单:`/actuator/**`(健康检查)、`/api/*/callback/**`(第三方 webhook)
+
+#### Scenario: 注解豁免
+
+- **WHEN** 目标 Controller 方法或其类带 `@NoAuth` 注解
+- **THEN** 签名校验跳过
+- **AND** 注解识别发生在 `HandlerInterceptor.preHandle`(能访问到 `HandlerMethod`)
+
+### Requirement: 密钥配置
+
+`secret` MUST 通过环境变量注入,禁止硬编码。`enabled=true` 但 `secret` 为空时 MUST 拒绝启动或拒绝请求。
+
+#### Scenario: secret 未配置
+
+- **WHEN** `mjava.auth.enabled=true` 但 `mjava.auth.secret` 为空或未设置
+- **THEN** 启动时打 ERROR 日志
+- **AND** 所有受保护请求返回 `503 { code: "AUTH_CONFIG_MISSING" }`
+
+#### Scenario: secret 从环境变量读取
+
+- **WHEN** `application.yml` 中 `secret: ${AUTH_SECRET}`
+- **AND** 部署环境设置 `AUTH_SECRET` 变量
+- **THEN** 启动时成功加载,后续请求按此密钥校验

+ 59 - 0
openspec/changes/add-request-auth-replay-guard/tasks.md

@@ -0,0 +1,59 @@
+## 1. 签名工具与 Nonce 缓存
+
+- [ ] 1.1 新建 `mjava/src/main/java/com/malk/utils/UtilSignature.java`:HMAC-SHA256 静态方法 `sign(secret, method, path, body, timestamp, nonce)` → hex
+- [ ] 1.2 新建 `mjava/src/main/java/com/malk/core/NonceCache.java`:封装 `UtilToken.TimedCache`,API 为 `putIfAbsent(nonce) → boolean`
+- [ ] 1.3 单元测试:`UtilSignatureTest` 覆盖不同 method / 空 body / 中文 path
+- [ ] 1.4 单元测试:`NonceCacheTest` 覆盖 TTL 过期 / LRU 淘汰 / 并发
+
+## 2. 配置与注解
+
+- [ ] 2.1 新建 `com.malk.config.AuthConfigProperties`(`@ConfigurationProperties(prefix = "mjava.auth")`)
+- [ ] 2.2 新建 `com.malk.config.AuthConfig`(`@Configuration`,根据 enabled 与 secret 条件装配)
+- [ ] 2.3 新建注解 `com.malk.filter.NoAuth`(`@Target({METHOD, TYPE}) @Retention(RUNTIME)`)
+- [ ] 2.4 `application.yml` 补默认值 `mjava.auth.enabled: false`
+- [ ] 2.5 各子模块 `application-*.yml.example` 示例 `mjava.auth.*` 占位
+
+## 3. Filter + Interceptor 两层
+
+- [ ] 3.1 新建 `com.malk.filter.AuthFilter`:Header 齐全 + 时间戳窗 + Nonce 去重;失败抛 `McException` 或直接写 `McR.fail()` 响应
+- [ ] 3.2 注册 `AuthFilter` 到 `WebConfiguration`,顺序在 `TraceIdFilter` 之后、业务之前
+- [ ] 3.3 新建 `com.malk.filter.AuthInterceptor`:`preHandle` 中签名校验 + `@NoAuth` 识别
+- [ ] 3.4 注册 `AuthInterceptor` 到 `WebMvcConfigurer`,排除 `exempt-paths`
+- [ ] 3.5 错误响应统一 `McR` 格式(见 design.md 错误码表)
+
+## 4. 审计日志集成
+
+- [ ] 4.1 `AuthFilter` / `AuthInterceptor` 失败路径打 WARN 日志,字段:`authKey` / `failReason` / `clientIp` / `path` / `timestamp`
+- [ ] 4.2 成功路径向 MDC 写 `authKey`,`TraceIdFilter` 已有链路串联
+- [ ] 4.3 logback-spring.xml pattern 追加 `[%X{authKey:-}]`
+
+## 5. 参考客户端实现(文档侧)
+
+- [ ] 5.1 Java 客户端签名示例(工具类形式,便于其他 Java 系统 copy)
+- [ ] 5.2 Python 客户端签名示例(`requests` + `hmac`)
+- [ ] 5.3 Node.js 客户端签名示例(`node:crypto`)
+- [ ] 5.4 curl + openssl 命令行示例(方便临时调试)
+
+## 6. 回归测试
+
+- [ ] 6.1 `AuthFilterTest`:enabled/disabled 切换、Header 缺失、时间窗边界、Nonce 重放
+- [ ] 6.2 `AuthInterceptorTest`:`@NoAuth` 方法级/类级、签名校验、exempt-paths
+- [ ] 6.3 集成测试:mjava-mcli 开启后用签名客户端调一次 → 成功;改签名 → 403;改时间戳 → 401
+- [ ] 6.4 mcli/shunfeng/guangming **保持 `enabled: false`** 场景回归(零破坏验证)
+
+## 7. 文档
+
+- [ ] 7.1 `/Users/malk/Desktop/Tech/claude/后端/mjava-baseline.md` 新增 §13(或合并到 §6)"请求鉴权与防重放"章节,含签名协议与开关
+- [ ] 7.2 README.md(本 change 完成后):客户端签名接入指引
+- [ ] 7.3 BACKLOG.md 更新专项状态
+
+## 8. 验证
+
+- [ ] 8.1 `openspec validate add-request-auth-replay-guard --strict` 通过
+- [ ] 8.2 开启 enabled 后用 `curl + openssl` 跑通一次真实请求
+- [ ] 8.3 Nonce 重放测试:同签名连发两次 → 第二次 401
+
+## 9. 交付
+
+- [ ] 9.1 PR 附签名协议示意图
+- [ ] 9.2 走 `/opsx:archive`