Appearance
签名规范
TIP
系统只有一套签名算法,用于所有商户 API 请求(创建交易、退款、订阅等)。 Webhook 回调签名是另一套独立算法(见 Webhook 规范)。
算法概述
算法: SHA-256(十六进制输出,非 HMAC)
密钥使用方式: SecretId 追加到字符串末尾(不是 HMAC 密钥)
签名步骤
- 取请求所有参数(
sign字段本身不参与签名) - 按字段名 ASCII 字典序升序排序
- 只取
String和Boolean类型字段的值依次拼接(BigDecimal、Integer等数值类型不参与) - 在末尾追加
SecretId - 对整个字符串进行 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...A1abcdefgSHA-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 嵌套字段(如 cardInfo、items)如何参与签名?
A: 这些字段在请求中是 String 类型(JSON 序列化后的字符串),整体作为字符串值参与签名,不需要解析内部字段。
Q: Boolean 类型如何拼接?
A: 直接使用其字符串表示,如 true 或 false。
Q: 空字符串字段是否参与签名?
A: 空字符串("")属于 String 类型,参与签名,拼接时贡献空字符串。null 值建议不传该字段。
