This commit is contained in:
wanghu
2026-09-02 09:38:45 +08:00
commit 8774b6cc44
128 changed files with 9382 additions and 0 deletions
@@ -0,0 +1,71 @@
# yh-platform 签名协议规范(v1.0)
> 权威源:Java `yh-utils-core` `utils.crypto.SignUtil` + `yh-app-auth-spring-boot-starter` `SignCanonical`。
> 本文档为双端(Java / C#)共同遵守的契约工件;任何变更须先改本文,再同步两侧实现。
## 1. 算法
统一使用 **HmacSHA256**,密钥与签名串均为 UTF-8 字节。
## 2. 双签名规则(长期并存,见方案 9.5 裁决 #5 收敛计划)
### 2.1 数据级签名(业务内容签名)
```
source = data + "|" + timestamp
output = HmacSHA256(source, secret) 的小写十六进制
timestamp = 毫秒级时间戳字符串
```
- 验签忽略大小写比对(`equalsIgnoreCase`)。
- `verifyWithExpire`:先比对签名,再校验 `|now - ts| <= expireMs`(默认 5 分钟,双向偏差均拒绝)。
### 2.2 请求级签名(HTTP 请求签名,SDK 出站默认)
```
canonicalTarget = path + ("?" + sortedQuery # query 为空则不带
source = METHOD + "\n" + canonicalTarget + "\n" + timestamp
output = HmacSHA256(source, secret) 的 Base64
```
- `METHOD`:大写(GET/POST),**大小写敏感**精确比对(这是既有行为,是否收敛见方案 9.5 裁决 #3)。
- `path`:**只取路径**,不含 host/port/context-path(保证服务发现/反代前后端天然对齐)。
- **basePath 剥离(客户端与服务端共同口径)**:客户端签名前从 path 剥离部署 basePath(与 UserBase 部署 context-path 一致,如 `/userbase`,对齐 Java `AppSignInterceptor.stripBasePath`);服务端验签前剥 `request.getContextPath()`(对齐 Java `AppAuthFilter`)。剥离后 canonical 口径统一为 `/oapi/**`。剥离规则:path 不以 basePath 开头时原样保留;剥离后为空或丢失前导斜杠时补 `/`(`/userbase` → `/`)。
- `sortedQuery`:query 按 `k=v` 整串字典序排序后以 `&` 连接;k、v 先 URL 解码再参与排序;多值参数的值也排序。
- 验签为**精确比对**(Base64 区分大小写)。
- `verifyRequestWithExpire` 语义同数据级。
- 测试向量中的 `canonicalTarget` 即**剥离后**的服务端口径(含 `fullUrl`/`basePath` 字段可复现剥离前 URL)。
### 2.3 密钥约束
- secret 最小长度 **16** 位(`MIN_SECRET_LENGTH`),低于此值签名方法抛参数异常。
## 3. HTTP 头契约(逐字一致)
| 头名 | 值 |
|---|---|
| `X-App-Key` | 调用方在 UserBase 注册的 appKey |
| `X-Timestamp` | 毫秒级时间戳字符串 |
| `X-Signature` | 请求级签名(Base64) |
## 4. 防重放
- 时间窗口:`|now - X-Timestamp| > expireMs`(默认 **300000ms**)即拒绝。
- nonce(可选扩展):同一 `(appKey, timestamp, nonce)` 在窗口内只允许提交一次;
Redis 键 `sign:replay:{appKey}:{timestamp}:{nonce}`,SETNX + TTL=窗口。
## 5. 验签密钥解析链(服务端)
```
1. Redis key = sign:app:{appKey} → 值为 SignAppKeyPayload JSON {appKey, secret, enabled}
2. 进程内缓存(Caffeine / IMemoryCache,TTL 5min)
3. 兜底 GET {userbase}/oapi/internal/sign-key/{appKey} → 信封 data 为 SignAppKeyPayload
```
- 仅当 `enabled=true` 且 `secret` 非空才可用;未知 appKey 直接拒绝。
- 任一层异常静默降级到下一层(Redis 宕机不阻断业务)。
## 6. 测试向量
`fixtures/sign-vectors.json`:≥10 组向量(含空 body、超长参数、中文 UTF-8、query 排序、密钥长度边界、毫秒时间戳边界)。
**基线由 Java `SignUtil` 实际运行产出**;CI 中 Java 侧重算 fixture 防漂移,C# 侧逐字节对拍。