签名算法
所有需要鉴权的 API 请求都必须携带签名,平台通过签名验证请求的合法性和完整性。
算法概述
| 项目 | 说明 |
|---|---|
| 算法 | HMAC-SHA256 |
| 密钥 | AppSecret |
| 编码 | 签名结果转为 Base64 字符串 |
签名步骤
第一步:拼接待签名字符串
将以下字段按顺序 直接拼接(无分隔符、无键名):
待签名字符串 = AppKey + Timestamp + Nonce + RequestBody| 字段 | 来源 | 说明 |
|---|---|---|
| AppKey | X-App-Key Header | 应用标识 |
| Timestamp | X-Timestamp Header | Unix 时间戳(秒级) |
| Nonce | X-Nonce Header | 随机字符串 |
| RequestBody | HTTP 请求体 | GET 请求或无 Body 时为空字符串 |
第二步:使用 HMAC-SHA256 计算签名
sign = Base64( HMAC-SHA256( AppSecret, 待签名字符串 ) )第三步:将签名放入请求头
X-Sign: {sign}完整示例
参数准备
| 参数 | 值 |
|---|---|
| AppKey | app_test_001 |
| AppSecret | secret_abc_123 |
| Timestamp | 1710000000 |
| Nonce | a1b2c3d4e5 |
GET 请求签名
请求:GET /open-api/merchant/info?id=1001
待签名字符串(无 Body,末尾为空):
app_test_0011710000000a1b2c3d4e5签名计算:
sign = Base64(HMAC-SHA256("secret_abc_123", "app_test_0011710000000a1b2c3d4e5"))POST 请求签名
请求:POST /open-api/order/create
请求体:
json
{"merchantId":1001,"storeId":2001,"totalAmount":29900}待签名字符串:
app_test_0011710000000a1b2c3d4e5{"merchantId":1001,"storeId":2001,"totalAmount":29900}签名计算:
sign = Base64(HMAC-SHA256("secret_abc_123", "app_test_0011710000000a1b2c3d4e5{\"merchantId\":1001,\"storeId\":2001,\"totalAmount\":29900}"))各语言实现
Java
java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.UUID;
public class OpenApiSigner {
public static String sign(String appSecret, String appKey,
String timestamp, String nonce, String body) throws Exception {
String dataToSign = appKey + timestamp + nonce + (body != null ? body : "");
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(appSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] hash = mac.doFinal(dataToSign.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(hash);
}
public static void main(String[] args) throws Exception {
String appKey = "app_test_001";
String appSecret = "secret_abc_123";
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
String nonce = UUID.randomUUID().toString().replace("-", "");
String body = "{\"orderId\":\"20240301001\"}";
System.out.println("X-Sign: " + sign(appSecret, appKey, timestamp, nonce, body));
}
}Python
python
import hmac
import hashlib
import base64
import time
import uuid
def sign(app_secret: str, app_key: str, timestamp: str, nonce: str, body: str = "") -> str:
data_to_sign = app_key + timestamp + nonce + body
signature = hmac.new(
app_secret.encode("utf-8"),
data_to_sign.encode("utf-8"),
hashlib.sha256
).digest()
return base64.b64encode(signature).decode("utf-8")
# 示例
app_key = "app_test_001"
app_secret = "secret_abc_123"
timestamp = str(int(time.time()))
nonce = uuid.uuid4().hex
body = '{"orderId":"20240301001"}'
print("X-Sign:", sign(app_secret, app_key, timestamp, nonce, body))Node.js
javascript
const crypto = require('crypto');
function sign(appSecret, appKey, timestamp, nonce, body = '') {
const dataToSign = appKey + timestamp + nonce + body;
return crypto
.createHmac('sha256', appSecret)
.update(dataToSign, 'utf8')
.digest('base64');
}
// 示例
const appKey = 'app_test_001';
const appSecret = 'secret_abc_123';
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomUUID().replace(/-/g, '');
const body = '{"orderId":"20240301001"}';
console.log('X-Sign:', sign(appSecret, appKey, timestamp, nonce, body));cURL
bash
#!/bin/bash
APP_KEY="app_test_001"
APP_SECRET="secret_abc_123"
TIMESTAMP=$(date +%s)
NONCE=$(uuidgen | tr -d '-')
BODY='{"orderId":"20240301001"}'
DATA_TO_SIGN="${APP_KEY}${TIMESTAMP}${NONCE}${BODY}"
SIGN=$(echo -n "$DATA_TO_SIGN" | openssl dgst -sha256 -hmac "$APP_SECRET" -binary | base64)
curl -X POST "https://api.example.com/open-api/order/create" \
-H "Content-Type: application/json" \
-H "X-App-Key: $APP_KEY" \
-H "X-Timestamp: $TIMESTAMP" \
-H "X-Nonce: $NONCE" \
-H "X-Sign: $SIGN" \
-d "$BODY"常见问题
签名验证失败排查
- 拼接顺序:必须是
AppKey + Timestamp + Nonce + Body,顺序不能错 - 无分隔符:字段之间直接拼接,不要加
&、=或其他分隔符 - 编码方式:签名结果是 Base64 编码,不是 Hex
- Body 原文:POST 请求的 Body 必须是发送的原始 JSON 字符串,保持紧凑格式
- GET 请求:无 Body 时,待签名串末尾为空(即只有
AppKey + Timestamp + Nonce) - 字符编码:所有字符串使用 UTF-8 编码
- 时间戳:使用秒级时间戳,不是毫秒级
- AppSecret:确认使用正确的密钥,注意是否已被重置