Appearance
接口签名设计方案文档
1. 设计目的
本方案旨在提供安全的接口访问机制,主要解决以下问题:
- 防恶意刷接口:防止攻击者利用工具恶意调用平台接口
- 数据完整性保障:确保请求参数在传输过程中不被篡改
- 防重放攻击:确保每个请求的唯一性和时效性
- 资源保护:避免数据库资源被无效请求浪费
2. 签名参数说明
参数名 | 类型 | 必传 | 说明 |
---|---|---|---|
appkey | string | 是 | 平台分配的应用程序标识 |
timestamp | string | 是 | 校准后的客户端时间戳(毫秒) |
noncestr | string | 是 | 8 位随机字符串(a-z0-9) |
signature | string | 是 | 基于请求参数生成的 MD5 签名 |
3. 签名生成流程
3.1 时间校准(首次加载时执行)
3.2 请求签名生成(每次接口请求)
javascript
import { Md5 } from "ts-md5";
const APP_KEY = "BC001CMEA007"; // 平台分配的密钥
function generateSignature(params: object) {
// 1. 获取时间偏差值
const offset = Number(sessionStorage.getItem("timeOffset") || 0);
// 2. 计算校准时间戳
const timestamp = Date.now() - offset;
// 3. 生成8位随机字符串
const noncestr = Math.random().toString(36).slice(2, 10);
// 4. 参数标准化处理
const paramsStr = normalizeParams(params);
// 5. 生成签名
const signature = Md5.hashStr(
`${APP_KEY}${timestamp}${noncestr}${paramsStr}`
).toString();
return { timestamp, noncestr, signature };
}
3.3 参数标准化算法
typescript
function normalizeParams(params: Record<string, any>, prefix = ""): string {
const segments: string[] = [];
// 过滤无效值并按键名排序
const validKeys = Object.keys(params)
.filter((key) => params[key] != null && params[key] !== "")
.sort((a, b) => a.localeCompare(b, "en"));
for (const key of validKeys) {
const value = params[key];
const currentPath = prefix ? `${prefix}.${key}` : key;
if (Array.isArray(value)) {
// 处理数组类型
for (let i = 0; i < value.length; i++) {
const element = value[i];
if (element == null || element === "") continue;
if (typeof element === "object") {
segments.push(normalizeParams(element, `${currentPath}[${i}]`));
} else {
segments.push(`${currentPath}[${i}]=${element}`);
}
}
} else if (typeof value === "object") {
// 处理嵌套对象
segments.push(normalizeParams(value, currentPath));
} else {
// 处理基本类型
segments.push(`${currentPath}=${value}`);
}
}
return segments.filter(Boolean).join("&");
}
3.4 请求示例
javascript
// 请求参数
const requestParams = {
userId: 123,
search: {
keywords: "test",
filters: [1, 2],
categories: [{ id: 5 }, { id: 7 }],
},
};
// 生成签名
const { timestamp, noncestr, signature } = generateSignature(requestParams);
// 设置请求头
axios.post("/api/data", requestParams, {
headers: {
appkey: APP_KEY,
timestamp,
noncestr,
signature,
},
});
4. 服务端验证流程
4.1 验证步骤
4.2 验证逻辑(Java 示例)
java
public boolean verifySignature(HttpServletRequest request) {
// 1. 获取请求头参数
String appkey = request.getHeader("appkey");
String timestampStr = request.getHeader("timestamp");
String noncestr = request.getHeader("noncestr");
String clientSignature = request.getHeader("signature");
// 2. 检查必传参数
if (StringUtils.isAnyBlank(appkey, timestampStr, noncestr, clientSignature)) {
return false;
}
try {
long timestamp = Long.parseLong(timestampStr);
// 3. 验证时间有效性(5分钟有效期)
long currentTime = System.currentTimeMillis();
if (Math.abs(currentTime - timestamp) > 300000) { // 5分钟
return false;
}
// 4. 验证AppKey有效性
if (!validAppKeys.contains(appkey)) {
return false;
}
// 5. 获取请求参数并标准化
Map<String, Object> params = getRequestParams(request);
String paramsStr = normalizeParams(params);
// 6. 生成服务端签名
String rawString = appkey + timestamp + noncestr + paramsStr;
String serverSignature = DigestUtils.md5Hex(rawString);
// 7. 比对签名
return serverSignature.equalsIgnoreCase(clientSignature);
} catch (Exception e) {
return false;
}
}
5. 安全策略
5.1 防攻击措施
措施 | 防护目标 | 实现方式 |
---|---|---|
时间窗口限制 | 防重放攻击 | 服务端验证时间戳 ±5 分钟 |
随机字符串(noncestr) | 确保请求唯一性 | 每次请求生成 8 位随机字符 |
参数完整性签名 | 防参数篡改 | 所有参数参与签名生成 |
AppKey 白名单 | 识别合法客户端 | 服务端维护有效 AppKey 列表 |
请求限流 | 防高频请求攻击 | 基于 IP 或 AppKey 的请求频率限制 |
5.2 敏感数据处理
- 加密传输:对敏感参数(如密码、token)使用 AES 加密
- 参数过滤:签名前过滤空值参数(null, undefined, "")
- 错误信息模糊:验证失败时返回统一错误信息,避免信息泄露
6. 注意事项
- 时间同步:确保客户端-服务端时间误差在允许范围内
- 参数标准化一致性:客户端与服务端必须使用完全相同的标准化算法
- 密钥管理:
- AppKey 应定期轮换
- 禁止在客户端硬编码敏感密钥
- 算法升级:
- 签名算法变更时需保证向后兼容
- 建议提供版本号参数便于扩展
- 性能优化:
- 服务端缓存已验证签名(结合 noncestr)
- 对超大请求体进行特殊处理
7. 错误代码规范
HTTP 状态码 | 错误代码 | 说明 |
---|---|---|
400 | 10001 | 缺少必要的签名参数 |
401 | 10002 | 签名验证失败 |
401 | 10003 | 时间戳已过期(超出 5 分钟范围) |
401 | 10004 | 无效的 AppKey |
429 | 10005 | 请求频率超限 |
附录:参数标准化示例
输入参数:
json
{
"name": "John",
"age": 30,
"contacts": {
"email": "john@example.com",
"phones": ["123456", null, "789012"]
},
"hobbies": ["reading", "sports"],
"metadata": {
"createdAt": 1650000000,
"tags": [1, 2, 3]
}
}
标准化输出:
search.categories[0].id=5&search.categories[1].id=7&search.filters[0]=1&search.filters[1]=2&search.keywords=test&userId=123
注意:所有参数按键名 ASCII 排序,空值被过滤,数组索引保持连续