签名说明
为保证数据传输的安全,我们对请求参数进行签名验证。
签名算法
我们使用 HMAC-SHA256 签名算法。请求中必须包含 sign_type 参数:
{
"platform_id": "PF0002",
"sign_type": "HMAC-SHA256",
"sign": "..."
}
范例商户信息
以下所有示例使用相同的测试商户信息:
| 项目 | 值 |
|---|---|
| 商户 ID (platform_id) | PF0002 |
| 平台密钥 (platform_key) | ThisIsYourSecretKey123 |
签名规则
- 参数整理:将所有参数按参数名 ASCII 码由小到大排序
- 排除参数:以下参数不参与签名:
sign、sign_type- 值为
null或空字符串""的参数 - 未出现在 body 中的参数——缺省的 key 本来就不存在,自然不参与签名(不要为它补一个
key=占位)
- 拼接字符串:以
key=value形式用&连接(不要做 URL encoding,使用原始值) - HMAC-SHA256 签名:使用密钥对字符串进行 HMAC-SHA256 签名
- 转小写:将结果转为 64 位小写十六进制字符串
「不参与签名」的判定标准是 该 key 在签名字符串中是否出现,三种情况一律剔除,不可只剔除其中一种:
| 情况 | 范例 | 是否参与签名 |
|---|---|---|
值为 null | "remark": null | ❌ 不参与 |
| 值为空字符串 | "remark": "" | ❌ 不参与 |
| key 根本没出现 | body 中完全没有 remark | ❌ 不参与 |
| 值为有效内容 | "remark": "test" | ✅ 参与 |
因此请遍历实际 body 内有返回值的字段动态生成签名字符串,不要把字段列表写死——这样无论字段是缺省、为空、还是未来新增,签名逻辑都不需要改动。
陣列參數處理
當請求參數包含陣列(如 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);
$pairs = [];
foreach ($filtered as $k => $v) {
$pairs[] = $k . '=' . $v;
}
$paramStr = implode('&', $pairs);
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();
}
验证回调签名
收到回调通知时,请验证签名以确保数据未被篡改:
- 使用 HMAC-SHA256 算法重新计算签名
- 比对计算结果与
sign是否一致
务必验证回调签名,防止伪造回调攻击。
回调的动态字段
回调通知的 body 字段不固定——部分字段会依订单状态、上游能否取得资讯而变动。验签时请遵循与请求签名相同的规则:遍历实际收到的 body 字段动态计算签名,不可将字段列表写死。
范例:付款人资讯通知(payment_cl_name_notify_url)的 body 中,payment_cl_number(付款人银行账号)、payment_cl_bank_name(开户银行代码)属于可选字段——上游无法取得时,该 key 会直接从 body 省略,而不是传 null 或 ""。
同一个接口可能收到两种 body:
// 情况 A:银行资讯可得 —— 5 个字段全部参与签名
{
"payment_id": "PM00000102",
"payment_cl_id": "97a968b4a9db497c8c03198e395a38c6",
"payment_cl_name": "TRAN TRUNG HIEU",
"payment_cl_number": "10066777777",
"payment_cl_bank_name": "MB",
"sign": "..."
}
// 情况 B:仅姓名可得 —— payment_cl_number / payment_cl_bank_name 整个 key 不出现,
// 签名字符串只拼接实际存在的 3 个字段
{
"payment_id": "PM00000103",
"payment_cl_id": "a1c2d3e4f5g6h7i8",
"payment_cl_name": "NGUYEN VAN A",
"sign": "..."
}
情况 B 的签名字符串只包含 payment_cl_id、payment_cl_name、payment_id 三个字段——绝不能因为「文档里有 payment_cl_number」就硬补一个 payment_cl_number= 进去,否则验签必然失败。未来若新增字段(如付款时间、银行分行),动态遍历的写法同样无需改动。
常见问题
签名错误 (error_code: 0004)
常见原因:
- 参数值做了 URL encoding - 签名字符串使用原始值拼接,不要做 URL encoding(例如
notify_url应保持https://...原样,而非https%3A%2F%2F...) - 参数值不一致 - 确保签名时的参数值与实际请求完全一致
- 未排除 sign_type - 签名计算时应排除
sign和sign_type - 编码问题 - 确保使用 UTF-8 编码
- 为空值 / 缺省字段补占位 - 值为
null、空字符串"",或根本未出现在 body 中的字段,都不应进入签名字符串;尤其验签回调时,不要因为文档列出某字段就硬补key=