|
|
@@ -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 描述里写明理由 |
|