请求头固定格式:
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。两种解决方案:
AuthFilter 只做 Header 齐全性与时间戳/Nonce 校验;签名校验延后到 HandlerInterceptor.preHandle(那时能拿到 HandlerMethod,可读 @NoAuth)AuthFilter 一次到位,@NoAuth 只支持类级别(通过 RequestMappingHandlerMapping 提前解析)Phase 1 选方案 A(拆分两层),让注解豁免精确到方法。
NonceCache 基于 UtilToken.TimedCache:
nonce:{X-MJ-Nonce}X-MJ-Key,便于日志)mjava.auth.window + 30s(窗口 + 冗余 30s,避免边界时间戳造成"刚过期还被接受")nonce-cache-size,LRU 淘汰Nonce 已存在 → 判定重放。
三种豁免方式(优先级从上到下):
enabled: false(全局开关):完全不做校验exempt-paths(路径匹配):Ant 风格,/actuator/** / /api/*/callback/**@NoAuth 注解:方法级或类级,粒度最细mjava.auth.secret,通过环境变量 AUTH_SECRET 注入secret + secret-next),客户端切完后再移除旧密钥。本 change 不做换密钥能力(YAGNI),需要时再加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 缺" 不告诉缺哪个、"签名错" 不告诉期望值),避免给攻击者定位线索。日志侧记录详情。
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 → 灰度切换CallerRateLimiter 是独立专项,见 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 描述里写明理由 |