Skip to content

接口签名设计方案文档

1. 设计目的

本方案旨在提供安全的接口访问机制,主要解决以下问题:

  • 防恶意刷接口:防止攻击者利用工具恶意调用平台接口
  • 数据完整性保障:确保请求参数在传输过程中不被篡改
  • 防重放攻击:确保每个请求的唯一性和时效性
  • 资源保护:避免数据库资源被无效请求浪费

2. 签名参数说明

参数名类型必传说明
appkeystring平台分配的应用程序标识
timestampstring校准后的客户端时间戳(毫秒)
noncestrstring8 位随机字符串(a-z0-9)
signaturestring基于请求参数生成的 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. 注意事项

  1. 时间同步:确保客户端-服务端时间误差在允许范围内
  2. 参数标准化一致性:客户端与服务端必须使用完全相同的标准化算法
  3. 密钥管理
    • AppKey 应定期轮换
    • 禁止在客户端硬编码敏感密钥
  4. 算法升级
    • 签名算法变更时需保证向后兼容
    • 建议提供版本号参数便于扩展
  5. 性能优化
    • 服务端缓存已验证签名(结合 noncestr)
    • 对超大请求体进行特殊处理

7. 错误代码规范

HTTP 状态码错误代码说明
40010001缺少必要的签名参数
40110002签名验证失败
40110003时间戳已过期(超出 5 分钟范围)
40110004无效的 AppKey
42910005请求频率超限

附录:参数标准化示例

输入参数:

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 排序,空值被过滤,数组索引保持连续