Skip to content

认证机制

2.1 认证方式

采用 AppKey + HMAC-SHA256 签名 认证。每个请求都需要携带以下请求头:

请求头必填说明
X-App-Key平台分配的应用标识
X-Timestamp请求发起时的 Unix 时间戳(秒),与服务器时间差不得超过 5 分钟
X-Nonce随机字符串(建议 UUID),用于防重放,同一 nonce 10 分钟内不可重复使用
X-SignatureHMAC-SHA256 签名值
Content-Type固定为 application/json

2.2 签名算法

第一步:拼接签名字符串

sign_string = "{app_key}\n{timestamp}\n{nonce}\n{method}\n{path}\n{sorted_query}\n{body_md5}"

各字段说明:

字段说明示例
app_key你的应用标识crm_system_001
timestamp与 X-Timestamp 请求头一致1722844800
nonce与 X-Nonce 请求头一致a1b2c3d4e5f6
methodHTTP 方法(大写)GETPOST
path请求路径(不含域名)/api/open/v1/guests
sorted_query将原始 query string 按 & 分割后按字母序排列再拼接;必须使用 URL 编码后的原始值(不要解码后重新编码);无 query 参数时为空字符串city=%E4%B8%8A%E6%B5%B7&page=1
body_md5请求体的 MD5 摘要(小写 hex);GET 请求无 body 时使用空字符串的 MD5d41d8cd98f00b204e9800998ecf8427e

注意

  • body_md5 是对原始请求体(raw body)计算 MD5,确保签名时使用的 body 与实际发送的 body 完全一致。
  • sorted_query 必须参与签名,防止攻击者在不变签名的情况下篡改 URL 查询参数。

第二步:计算 HMAC-SHA256

signature = HMAC-SHA256(key=app_secret, message=sign_string)

输出为小写十六进制字符串(64 位)。

2.3 签名示例

Python

python
import hashlib
import hmac
import time
import uuid
import json

def build_request_headers(
    app_key: str, app_secret: str,
    method: str, path: str,
    query_string: str = "", body: dict = None
) -> dict:
    """
    构建开放接口请求头
    
    :param query_string: 原始 query string(URL 编码后),如 "page=1&city=%E4%B8%8A%E6%B5%B7"
                         注意:必须使用原始编码,不要解码后重新拼接
    """
    timestamp = str(int(time.time()))
    nonce = uuid.uuid4().hex

    # 序列化 body
    if body:
        body_bytes = json.dumps(body, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
    else:
        body_bytes = b""

    # 规范化 query string(按 & 分割后按字母序排列,保持原始编码)
    if query_string:
        sorted_query = "&".join(sorted(query_string.split("&")))
    else:
        sorted_query = ""

    body_md5 = hashlib.md5(body_bytes).hexdigest()

    sign_string = f"{app_key}\n{timestamp}\n{nonce}\n{method}\n{path}\n{sorted_query}\n{body_md5}"
    signature = hmac.digest(
        app_secret.encode("utf-8"),
        sign_string.encode("utf-8"),
        "sha256"
    ).hex()

    return {
        "X-App-Key": app_key,
        "X-Timestamp": timestamp,
        "X-Nonce": nonce,
        "X-Signature": signature,
        "Content-Type": "application/json",
    }

Java

java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.util.*;

public class OpenApiClient {

    private String appKey;
    private String appSecret;

    /**
     * @param queryString 原始 query string(URL 编码后),如 "page=1&city=%E4%B8%8A%E6%B5%B7"
     *                    注意:必须使用原始编码,不要解码后重新拼接
     */
    public Map<String, String> buildHeaders(String method, String path,
                                            String queryString, String body) throws Exception {
        String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
        String nonce = UUID.randomUUID().toString().replace("-", "");

        // 规范化 query string(按 & 分割后按字母序排列,保持原始编码)
        String sortedQuery = "";
        if (queryString != null && !queryString.isEmpty()) {
            List<String> parts = new ArrayList<>(Arrays.asList(queryString.split("&")));
            Collections.sort(parts);
            sortedQuery = String.join("&", parts);
        }

        // 计算 body MD5
        String bodyMd5 = md5(body != null ? body : "");

        // 拼接签名字符串
        String signString = appKey + "\n" + timestamp + "\n" + nonce + "\n"
                          + method + "\n" + path + "\n" + sortedQuery + "\n" + bodyMd5;

        // HMAC-SHA256
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(appSecret.getBytes("UTF-8"), "HmacSHA256"));
        String signature = bytesToHex(mac.doFinal(signString.getBytes("UTF-8")));

        Map<String, String> headers = new HashMap<>();
        headers.put("X-App-Key", appKey);
        headers.put("X-Timestamp", timestamp);
        headers.put("X-Nonce", nonce);
        headers.put("X-Signature", signature);
        headers.put("Content-Type", "application/json");
        return headers;
    }

    private String md5(String input) throws Exception {
        MessageDigest md = MessageDigest.getInstance("MD5");
        byte[] digest = md.digest(input.getBytes("UTF-8"));
        return bytesToHex(digest);
    }

    private String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }
}

JavaScript / Node.js

javascript
const crypto = require('crypto');

/**
 * @param {string} queryString - 原始 query string(URL 编码后),如 "page=1&city=%E4%B8%8A%E6%B5%B7"
 *                               注意:必须使用原始编码,不要解码后重新拼接
 */
function buildHeaders(appKey, appSecret, method, path, queryString, body) {
    const timestamp = Math.floor(Date.now() / 1000).toString();
    const nonce = crypto.randomUUID().replace(/-/g, '');

    // 规范化 query string(按 & 分割后按字母序排列,保持原始编码)
    let sortedQuery = '';
    if (queryString) {
        sortedQuery = queryString.split('&').sort().join('&');
    }

    const bodyStr = body ? JSON.stringify(body) : '';
    const bodyMd5 = crypto.createHash('md5').update(bodyStr).digest('hex');

    const signString = `${appKey}\n${timestamp}\n${nonce}\n${method}\n${path}\n${sortedQuery}\n${bodyMd5}`;
    const signature = crypto
        .createHmac('sha256', appSecret)
        .update(signString)
        .digest('hex');

    return {
        'X-App-Key': appKey,
        'X-Timestamp': timestamp,
        'X-Nonce': nonce,
        'X-Signature': signature,
        'Content-Type': 'application/json',
    };
}

2.4 完整请求示例

python
import requests

APP_KEY = "crm_system_001"
APP_SECRET = "your_app_secret_here"
BASE_URL = "http://test-api.ailian.com"

# GET 请求(注意 query string 也参与签名)
path = "/api/open/v1/guests"
query_string = "page=1&page_size=20"  # 原始 query string,保持 URL 编码
headers = build_request_headers(APP_KEY, APP_SECRET, "GET", path, query_string=query_string)

response = requests.get(f"{BASE_URL}{path}?{query_string}", headers=headers)
print(response.json())

# POST 请求
path = "/api/open/v1/guests"
body = {
    "name": "张三",
    "gender": "male",
    "age": 28,
    "phone": "13800138000",
    "city": "上海"
}
headers = build_request_headers(APP_KEY, APP_SECRET, "POST", path, body=body)

response = requests.post(f"{BASE_URL}{path}", headers=headers, json=body)
print(response.json())

艾恋相亲 SaaS 平台