跳到主要内容

签名说明

为保证数据传输的安全,我们对请求参数进行签名验证。

签名算法

我们使用 HMAC-SHA256 签名算法。请求中必须包含 sign_type 参数:

{
"platform_id": "PF0002",
"sign_type": "HMAC-SHA256",
"sign": "..."
}

范例商户信息

以下所有示例使用相同的测试商户信息:

项目
商户 ID (platform_id)PF0002
平台密钥 (platform_key)ThisIsYourSecretKey123

签名规则

  1. 参数整理:将所有参数按参数名 ASCII 码由小到大排序
  2. 排除参数:以下参数不参与签名
    • signsign_type
    • 值为 null 或空字符串 "" 的参数
    • 未出现在 body 中的参数——缺省的 key 本来就不存在,自然不参与签名(不要为它补一个 key= 占位)
  3. 拼接字符串:以 key=value 形式用 & 连接(不要做 URL encoding,使用原始值)
  4. HMAC-SHA256 签名:使用密钥对字符串进行 HMAC-SHA256 签名
  5. 转小写:将结果转为 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&notify_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&notify_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();
}

验证回调签名

收到回调通知时,请验证签名以确保数据未被篡改:

  1. 使用 HMAC-SHA256 算法重新计算签名
  2. 比对计算结果与 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_idpayment_cl_namepayment_id 三个字段——绝不能因为「文档里有 payment_cl_number」就硬补一个 payment_cl_number= 进去,否则验签必然失败。未来若新增字段(如付款时间、银行分行),动态遍历的写法同样无需改动。


常见问题

签名错误 (error_code: 0004)

常见原因:

  1. 参数值做了 URL encoding - 签名字符串使用原始值拼接,不要做 URL encoding(例如 notify_url 应保持 https://... 原样,而非 https%3A%2F%2F...
  2. 参数值不一致 - 确保签名时的参数值与实际请求完全一致
  3. 未排除 sign_type - 签名计算时应排除 signsign_type
  4. 编码问题 - 确保使用 UTF-8 编码
  5. 为空值 / 缺省字段补占位 - 值为 null、空字符串 "",或根本未出现在 body 中的字段,都不应进入签名字符串;尤其验签回调时,不要因为文档列出某字段就硬补 key=