Skip to content

签名规范

TIP

系统只有一套签名算法,用于所有商户 API 请求(创建交易、退款、订阅等)。 Webhook 回调签名是另一套独立算法(见 Webhook 规范)。

算法概述

算法: SHA-256(十六进制输出,非 HMAC)

密钥使用方式: SecretId 追加到字符串末尾(不是 HMAC 密钥)

签名步骤

  1. 取请求所有参数(sign 字段本身不参与签名)
  2. 按字段名 ASCII 字典序升序排序
  3. 只取 StringBoolean 类型字段的值依次拼接(BigDecimalInteger 等数值类型不参与
  4. 在末尾追加 SecretId
  5. 对整个字符串进行 SHA-256 加密,输出小写十六进制字符串

IMPORTANT

payAmount 虽然是数字语义,但字段类型声明为 String,因此参与签名。 cycleLength(Integer)等数值类型字段不参与签名。

签名示例

请求参数(排序后):

cancelUrl    = "https://www.yoursite.com/cancel"
cardInfo     = "{...}"
clientIp     = "153.12.187.15"
currency     = "USD"
failedUrl    = "https://www.yoursite.com/failed"
items        = "[{...}]"
merchantId   = "888888888"
notifyUrl    = "https://www.yoursite.com/webhook"
payAmount    = "299"
paymentType  = "PAY"
relationId   = "39d9663d..."
requestId    = "REQ_001"
returnUrl    = "https://www.yoursite.com/success"

拼接所有值后,末尾追加 SecretId(如 A1abcdefg):

https://www.yoursite.com/cancel{...}153.12.187.15USD...888888888...299PAY...A1abcdefg

SHA-256 结果即为 sign 字段值。

代码示例

java
public static String computeSign(Map<String, Object> params, String secretId) {
    List<Map.Entry<String, Object>> entries = new ArrayList<>(params.entrySet());
    // 1. 排除 sign 字段,按 key ASCII 排序
    entries.sort((a, b) -> a.getKey().compareTo(b.getKey()));

    // 2. 只拼接 String 和 Boolean 类型的值
    StringBuilder sb = new StringBuilder();
    for (Map.Entry<String, Object> entry : entries) {
        if (entry.getKey().equals("sign")) continue;
        Object val = entry.getValue();
        if (val instanceof String || val instanceof Boolean) {
            sb.append(val);
        }
    }

    // 3. 追加 SecretId,SHA256 输出
    sb.append(secretId);
    return sha256Hex(sb.toString());
}
python
import hashlib

def compute_sign(params: dict, secret_id: str) -> str:
    # 1. 排除 sign 字段,过滤 String/Boolean 类型,按 key 排序
    filtered = {k: v for k, v in params.items()
                if k != 'sign' and isinstance(v, (str, bool))}
    sorted_keys = sorted(filtered.keys())

    # 2. 拼接值 + SecretId
    sign_str = ''.join(str(filtered[k]) for k in sorted_keys) + secret_id

    # 3. SHA256
    return hashlib.sha256(sign_str.encode('utf-8')).hexdigest()
javascript
async function computeSign(params, secretId) {
  const entries = Object.entries(params)
    .filter(([k, v]) => k !== 'sign' && (typeof v === 'string' || typeof v === 'boolean'))
    .sort(([a], [b]) => a.localeCompare(b))

  const signStr = entries.map(([, v]) => String(v)).join('') + secretId

  const buffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(signStr))
  return Array.from(new Uint8Array(buffer))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('')
}

常见问题

Q: JSON 嵌套字段(如 cardInfoitems)如何参与签名?

A: 这些字段在请求中是 String 类型(JSON 序列化后的字符串),整体作为字符串值参与签名,不需要解析内部字段。

Q: Boolean 类型如何拼接?

A: 直接使用其字符串表示,如 truefalse

Q: 空字符串字段是否参与签名?

A: 空字符串("")属于 String 类型,参与签名,拼接时贡献空字符串。null 值建议不传该字段。

Codrimpay