签名说明
MD5 签名算法即将弃用
2026/03/31 前,MD5 签名算法将被弃用。 请在此日期前迁移至 HMAC-SHA256。届时,使用 MD5 的请求将返回错误。
为保证数据传输的安全,我们对请求参数进行签名验证。
签名算法
我们支持以下两种签名算法:
| 算法 | sign_type 参数值 | 推荐程度 | 说明 |
|---|---|---|---|
| HMAC-SHA256 | HMAC-SHA256 | ⭐ 推荐 | 更安全的签名算法 |
| MD5 | MD5 或不传 | 兼容 | 为向后兼容保留 |
新商户建议
强烈建议使用 HMAC-SHA256,MD5 在现代密码学标准中已被视为不够安全。
如何选择签名算法
在请求中添加 sign_type 参数即可指定签名算法:
{
"platform_id": "PF0002",
"sign_type": "HMAC-SHA256",
"sign": "..."
}
不传 sign_type 时默认使用 MD5,保持向后兼容。
范例商户信息
以下所有示例使用相同的测试商户信息:
| 项目 | 值 |
|---|---|
| 商户 ID (platform_id) | PF0002 |
| 平台密钥 (platform_key) | ThisIsYourSecretKey123 |
HMAC-SHA256 签名(推荐)
签名规则
- 参数整理:将所有参数按参数名 ASCII 码由小到大排序
- 排除参数:
sign、sign_type、空值参数 - 拼接字符串:以
key=value形式用&连接 - HMAC-SHA256 签名:使用密钥对字符串进行 HMAC-SHA256 签名
- 转小写:将结果转为 64 位小写十六进制字符串
与 MD5 的差异
HMAC-SHA256 直接使用密钥进行签名,不需要将密钥追加到字符串末尾。
陣列參數處理
當請求參數包含陣列(如 last_numbers)時,需將陣列轉換為 JSON 字串格式參與簽名。
範例:last_numbers 為 ["12345", "67890"] 時:
last_numbers=["12345","67890"]
注意
陣列轉換為 JSON 字串時,不可有多餘空格,元素之間僅以逗號分隔。
代收示例
请求参数:
{
"platform_id": "PF0002",
"service_id": "SVC0001",
"payment_cl_id": "DEVPM00014581",
"amount": "50000",
"notify_url": "https://your-domain.com/callback",
"request_time": "1595504136",
"sign_type": "HMAC-SHA256"
}
步骤 1:排序并拼接(排除 sign、sign_type)
amount=50000¬ify_url=https://your-domain.com/callback&payment_cl_id=DEVPM00014581&platform_id=PF0002&request_time=1595504136&service_id=SVC0001
步骤 2:HMAC-SHA256 签名
使用密钥 ThisIsYourSecretKey123 签名,得到:
e8a5c3f2d1b4a6e9c7f0d2b5a8e1c4f7d0b3a6e9c2f5d8b1a4e7c0f3d6b9a2e5
代码示例
cURL
# 1. 拼接参数字符串(已排序,排除 sign 和 sign_type)
PARAM_STR="amount=50000¬ify_url=https://your-domain.com/callback&payment_cl_id=DEVPM00014581&platform_id=PF0002&request_time=1595504136&service_id=SVC0001"
PLATFORM_KEY="ThisIsYourSecretKey123"
# 2. HMAC-SHA256 签名
SIGN=$(echo -n "$PARAM_STR" | openssl dgst -sha256 -hmac "$PLATFORM_KEY" | awk '{print $2}')
echo $SIGN
Python
import hmac
import hashlib
def generate_sign_hmac_sha256(params: dict, platform_key: str) -> str:
filtered = {k: v for k, v in params.items()
if v and k not in ['sign', 'sign_type']}
sorted_keys = sorted(filtered.keys())
param_str = '&'.join([f'{k}={filtered[k]}' for k in sorted_keys])
return hmac.new(
platform_key.encode('utf-8'),
param_str.encode('utf-8'),
hashlib.sha256
).hexdigest().lower()
Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.*;
public class SignatureUtil {
public static String generateHmacSha256(Map<String, String> params, String key) {
TreeMap<String, String> filtered = new TreeMap<>();
for (Map.Entry<String, String> e : params.entrySet()) {
if (e.getValue() != null && !e.getValue().isEmpty()
&& !e.getKey().equals("sign") && !e.getKey().equals("sign_type")) {
filtered.put(e.getKey(), e.getValue());
}
}
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> e : filtered.entrySet()) {
if (sb.length() > 0) sb.append("&");
sb.append(e.getKey()).append("=").append(e.getValue());
}
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256"));
byte[] hash = mac.doFinal(sb.toString().getBytes("UTF-8"));
StringBuilder hex = new StringBuilder();
for (byte b : hash) hex.append(String.format("%02x", b));
return hex.toString();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
PHP
<?php
function generateSignHmacSha256(array $params, string $platformKey): string {
$filtered = array_filter($params, function($v, $k) {
return !empty($v) && !in_array($k, ['sign', 'sign_type']);
}, ARRAY_FILTER_USE_BOTH);
ksort($filtered);
$paramStr = http_build_query($filtered, '', '&');
return strtolower(hash_hmac('sha256', $paramStr, $platformKey));
}
?>
Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"sort"
"strings"
)
func GenerateSignHmacSha256(params map[string]string, platformKey string) string {
var keys []string
for k, v := range params {
if v != "" && k != "sign" && k != "sign_type" {
keys = append(keys, k)
}
}
sort.Strings(keys)
var pairs []string
for _, k := range keys {
pairs = append(pairs, k+"="+params[k])
}
paramStr := strings.Join(pairs, "&")
h := hmac.New(sha256.New, []byte(platformKey))
h.Write([]byte(paramStr))
return strings.ToLower(hex.EncodeToString(h.Sum(nil)))
}
JavaScript
const crypto = require('crypto');
function generateSignHmacSha256(params, platformKey) {
// 过滤并排序
const filtered = Object.entries(params)
.filter(([k, v]) => v && k !== 'sign' && k !== 'sign_type')
.sort(([a], [b]) => a.localeCompare(b));
// 拼接
const paramStr = filtered.map(([k, v]) => `${k}=${v}`).join('&');
// HMAC-SHA256
return crypto
.createHmac('sha256', platformKey)
.update(paramStr)
.digest('hex')
.toLowerCase();
}
MD5 签名(兼容旧版) - 点击展开
已不推荐
MD5 仅为向后兼容保留,新商户请使用 HMAC-SHA256。
签名规则
- 参数整理:将所有参数按参数名 ASCII 码由小到大排序
- 排除参数:
sign、sign_type、空值参数 - 拼接字符串:以
key=value形式用&连接 - 追加密钥:在末尾追加
&{platform_key}(不带 key= 前缀) - MD5 杂凑:转为 32 位小写字符串
代收示例
请求参数(不传 sign_type 则默认 MD5):
{
"platform_id": "PF0002",
"service_id": "SVC0001",
"payment_cl_id": "DEVPM00014581",
"amount": "50000",
"notify_url": "https://your-domain.com/callback",
"request_time": "1595504136"
}
步骤 1:排序并拼接
amount=50000¬ify_url=https://your-domain.com/callback&payment_cl_id=DEVPM00014581&platform_id=PF0002&request_time=1595504136&service_id=SVC0001
步骤 2:追加密钥
amount=50000¬ify_url=https://your-domain.com/callback&payment_cl_id=DEVPM00014581&platform_id=PF0002&request_time=1595504136&service_id=SVC0001&ThisIsYourSecretKey123
步骤 3:MD5 杂凑
49be5fa304b5f536c6e2ea89435e211a
代码示例
Python
import hashlib
def generate_sign_md5(params: dict, platform_key: str) -> str:
filtered = {k: v for k, v in params.items()
if v and k not in ['sign', 'sign_type']}
sorted_keys = sorted(filtered.keys())
param_str = '&'.join([f'{k}={filtered[k]}' for k in sorted_keys])
sign_str = f'{param_str}&{platform_key}'
return hashlib.md5(sign_str.encode('utf-8')).hexdigest().lower()
Java
import java.security.MessageDigest;
import java.util.*;
public class SignatureUtil {
public static String generateMd5(Map<String, String> params, String key) {
TreeMap<String, String> filtered = new TreeMap<>();
for (Map.Entry<String, String> e : params.entrySet()) {
if (e.getValue() != null && !e.getValue().isEmpty()
&& !e.getKey().equals("sign") && !e.getKey().equals("sign_type")) {
filtered.put(e.getKey(), e.getValue());
}
}
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> e : filtered.entrySet()) {
if (sb.length() > 0) sb.append("&");
sb.append(e.getKey()).append("=").append(e.getValue());
}
sb.append("&").append(key);
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(sb.toString().getBytes("UTF-8"));
StringBuilder hex = new StringBuilder();
for (byte b : digest) hex.append(String.format("%02x", b));
return hex.toString();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
验证回调签名
收到回调通知时,请验证签名以确保数据未被篡改:
- 从回调参数中提取
sign_type(如果有) - 使用相同的算法重新计算签名
- 比对计算结果与
sign是否一致
安全建议
务必验证回调签名,防止伪造回调攻击。
常见问题
签名错误 (error_code: 0004)
常见原因:
- MD5 密钥追加方式错误 - 应该是
&{platform_key}而不是&key={platform_key} - 参数值不一致 - 确保签名时的参数值与实际请求完全一致
- 未排除 sign_type - 签名计算时应排除
sign和sign_type - 编码问题 - 确保使用 UTF-8 编码