鉴权与签名
1. 概述
本平台采用 "公开凭证 + 保密密钥 + HMAC 签名" 的鉴权模型,适用于"机器对机器"(M2M)场景:调用方是开发者自己的服务器,需要的是接口级身份认证与防篡改,而非面向终端用户的 OAuth 授权。
2. 请求头定义
调用方在 HTTP 请求头中携带以下 4 个签名相关字段,另需携带 Content-Type: application/json:
| Header | 说明 |
|---|---|
| X-AccessKey | 收钱吧分配给开发者的公开凭证 |
| X-Timestamp | 请求时间戳(毫秒) |
| X-Nonce | 8 位随机字母数字串 |
| X-Signature | HMAC-SHA256 签名值(十六进制小写) |
3. 签名规范
3.1 签名算法
| 项目 | 说明 |
|---|---|
| 签名算法 | HMAC-SHA256,输出十六进制小写 |
| 密钥 | accessSecret(服务端存储的密钥明文) |
| 请求体 | 原始请求体字符串,无 body 时取空字符串 "" |
签名字符串由以下参数用 | 直接拼接而成,不含任何分隔空格:
accessKey | timestamp | nonce | requestBody
3.2 安全参数约束
| 参数 | 约束 |
|---|---|
| Timestamp | Unix 时间戳(毫秒),与服务器时间差 ≤ 5 分钟 |
| Nonce | 8 位随机字母数字串,每次请求不同,5 分钟内不可重复使用 |
| requestId | 全接口必传。写操作接口作为幂等键,24h 内相同 requestId 返回缓存结果;查询类接口用于链路追踪。详见幂等机制 |
4. 签名计算示例
4.1 Java
import org.apache.commons.codec.binary.Hex;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class HmacSignatureUtil {
private static final String ALGORITHM = "HmacSHA256";
/**
* 调用示例:生成签名并校验
*/
public static void main(String[] args) throws Exception {
String accessKey = "ak_live_xK9mP2nQ7wR4tY6z";
String accessSecret = "sk_live_aB3cD5eF7gH9jK1m";
String timestamp = String.valueOf(System.currentTimeMillis());
String nonce = "a1b2c3d4";
String requestBody = "{\"clientStoreSn\":\"SH001\",\"orderSn\":\"210000001000001\",\"requestId\":\"req_001\"}";
String signature = signature(accessKey, accessSecret, timestamp, nonce, requestBody);
System.out.println("signature = " + signature);
boolean verified = verify(accessKey, accessSecret, timestamp, nonce, requestBody, signature);
System.out.println("verify = " + verified);
}
/**
* 计算签名
*/
public static String signature(String accessKey, String accessSecret, String timestamp, String nonce,
String requestBody) throws NoSuchAlgorithmException, InvalidKeyException {
return hmacSign(buildSignString(accessKey, timestamp, nonce, requestBody), accessSecret);
}
/**
* 校验签名
*/
public static boolean verify(String accessKey, String accessSecret, String timestamp, String nonce,
String requestBody, String expectedSign) {
if (expectedSign == null) {
return false;
}
try {
String actualSign = signature(accessKey, accessSecret, timestamp, nonce, requestBody);
return MessageDigest.isEqual(actualSign.getBytes(StandardCharsets.UTF_8),
expectedSign.getBytes(StandardCharsets.UTF_8));
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
return false;
}
}
private static String buildSignString(String accessKey, String timestamp, String nonce, String requestBody) {
return accessKey + "|" + timestamp + "|" + nonce + "|" + (requestBody != null ? requestBody : "");
}
private static String hmacSign(String signString, String accessSecret)
throws NoSuchAlgorithmException, InvalidKeyException {
Mac mac = Mac.getInstance(ALGORITHM);
mac.init(new SecretKeySpec(accessSecret.getBytes(StandardCharsets.UTF_8), ALGORITHM));
byte[] signBytes = mac.doFinal(signString.getBytes(StandardCharsets.UTF_8));
return Hex.encodeHexString(signBytes);
}
}
4.2 Python
import hmac
import hashlib
import time
import random
import string
import json
def sign_request(access_key, access_secret, request_body):
timestamp = str(int(time.time() * 1000))
nonce = ''.join(random.choices(string.ascii_letters + string.digits, k=8))
sign_string = access_key + "|" + timestamp + "|" + nonce + "|" + (request_body if request_body else "")
signature = hmac.new(
access_secret.encode('utf-8'),
sign_string.encode('utf-8'),
hashlib.sha256
).hexdigest()
return {
'timestamp': timestamp,
'nonce': nonce,
'signature': signature
}
4.3 CURL 调用示例
# 1. 生成签名参数(用 Python 一行命令)
read TIMESTAMP NONCE SIGNATURE <<< $(python3 -c "
import hmac, hashlib, time, random, string
ts = str(int(time.time() * 1000))
nc = ''.join(random.choices(string.ascii_letters + string.digits, k=8))
ak = 'ak_live_xK9mP2nQ7wR4tY6z'
body = '{\"clientStoreSn\":\"SH001\",\"orderSn\":\"210000001000001\",\"requestId\":\"req_001\"}'
sg = hmac.new('sk_live_aB3cD5eF7gH9jK1m'.encode(), (ak + '|' + ts + '|' + nc + '|' + body).encode(), hashlib.sha256).hexdigest()
print(ts, nc, sg)
")
# 2. 发送请求
curl -X POST 'https://gateway-smart.shouqianba.com/open-api/v1/order/query' \
-H 'Content-Type: application/json' \
-H "X-AccessKey: ak_live_xK9mP2nQ7wR4tY6z" \
-H "X-Timestamp: $TIMESTAMP" \
-H "X-Nonce: $NONCE" \
-H "X-Signature: $SIGNATURE" \
-d '{"clientStoreSn":"SH001","orderSn":"210000001000001","requestId":"req_001"}'
5. 防重放机制
| 机制 | 说明 |
|---|---|
| Timestamp 窗口 | 请求时间戳与服务器时间差 ≤ 5 分钟,否则拒绝 |
| Nonce 去重 | 相同 accessKey + nonce 在 5 分钟内只允许出现一次 |
注意事项:
- 每次请求必须生成新的
nonce,不能复用 - 收到
41102(时间戳过期):检查系统时间是否准确 - 收到
41103(重复请求):检查是否重发了请求 - 签名错误时请先确认
accessSecret是否正确,再检查签名字符串拼接