鉴权与签名

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 是否正确,再检查签名字符串拼接

results matching ""

    No results matching ""