Files
2026-09-02 09:38:45 +08:00

72 lines
3.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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# 侧逐字节对拍。