签名算法 所有接口请求和平台发出的异步通知都使用同一套签名规则:HmacSHA256,输出小写十六进制。代收、代付规则完全一致,共用同一个 api_key。
签名步骤 Step 1 筛选参数
只取值为字符串 的字段。数字、布尔、null、对象、数组一律不参与签名(平台会直接忽略它们); 排除 sign 字段(签名本身不参与); 排除 extra 字段(扩展字段不参与); 排除值为空字符串的字段。 Step 2 排序
将剩余参数按 key 的 ASCII 码升序排列(区分大小写,按字节比较;不要用 localeCompare 或自然排序)。
Step 3 拼接待签名字符串
格式:key1=value1&key2=value2&key3=value3。值使用原文,不做 URL 编码、不去首尾空格 ;中文按 UTF-8 参与计算;首尾没有多余的 &。
Step 4 计算签名
以 api_key 作为 HMAC 密钥,对待签名字符串执行 HmacSHA256,输出小写十六进制 字符串(64 个字符):
sign = HmacSHA256(待签名字符串, api_key) 复制 所有字段一律按字符串传。 例如 "amount": 100 是数字,平台计算签名时会忽略它,而你的代码很可能把它拼进了待签名串,于是签名不一致;timestamp 按数字传还会直接报"时间戳缺失"。请写成 "amount": "100.00"、"timestamp": "1790900000"。
签名示例 以代付下单为例,假设 api_key 为 demo_api_key_123456,请求参数如下:
{
" merchant_code " : " MCH10001 " ,
" merchant_order_no " : " P20261002001 " ,
" product_code " : " BANK_PAYOUT " ,
" amount " : " 1000.00 " ,
" payee_name " : " 张三 " ,
" payee_account " : " 6222020200112233446 " ,
" payee_bank " : " 招商银行 " ,
" payee_alias " : "" ,
" notify_url " : " https://merchant.com/payout/callback " ,
" timestamp " : " 1790900000 " ,
" extra " : { " user_id " : " 12345 " },
" sign " : ""
} 复制 Step 1 — 排除 sign、extra、空值 payee_alias 后剩余 9 个字段:
amount=1000.00
merchant_code=MCH10001
merchant_order_no=P20261002001
notify_url=https://merchant.com/payout/callback
payee_account=6222020200112233446
payee_bank=招商银行
payee_name=张三
product_code=BANK_PAYOUT
timestamp=1790900000 复制 Step 2 — 已按 key 升序排列。
Step 3 — 拼接:
amount=1000.00&merchant_code=MCH10001&merchant_order_no=P20261002001¬ify_url=https://merchant.com/payout/callback&payee_account=6222020200112233446&payee_bank=招商银行&payee_name=张三&product_code=BANK_PAYOUT×tamp=1790900000 复制 Step 4 — 计算:
sign = HmacSHA256(待签名字符串, "demo_api_key_123456")
= 9ebeba22878514abfd77471467b126d2c9cba50fee8652dc260176f49dfce0fe 复制 在线计算 粘贴任意参数 JSON,逐步查看哪些字段参与签名、哪些被跳过:
[ 02 ] 签名计算器 SIGN CALCULATOR
⌁ 输入参数 JSON 后计算 逐步展示待签名串与签名
测试向量 以下 9 组数据均以 api_key = demo_api_key_123456 计算。你的签名实现对它们都能算出相同的 sign,就说明实现正确:
编号 场景 待签名串 → sign(api_key = demo_api_key_123456) V1 代付下单 amount=1000.00&merchant_code=MCH10001&merchant_order_no=P20261002001¬ify_url=https://merchant.com/payout/callback&payee_account=6222020200112233446&payee_bank=招商银行&payee_name=张三&product_code=BANK_PAYOUT×tamp=17909000009ebeba22878514abfd77471467b126d2c9cba50fee8652dc260176f49dfce0fe V2 代付查单 merchant_code=MCH10001&merchant_order_no=P20261002001×tamp=17909000605000e7d65ae86d54deb85319150db28316ebcfa5223d4738752e08e9d9d7383b V3 代付查余额 merchant_code=MCH10001×tamp=1790900060fb2116de1c6a386403430f07cb02fb39521126bb958783aebbcd9f080d839865 V4 代付成功通知 amount=1000.00&completed_at=2026-10-02 12:03:45&deduct_amount=1012.00&fee_amount=12.00&merchant_code=MCH10001&merchant_order_no=P20261002001&order_id=Ananpay20261002120000123a1b2c3d4&product_code=BANK_PAYOUT&status=914231c203ad9c58642b5424107bd50283691686041ac9e9c66b8b0cdffac6c93 V5 代付失败通知 amount=1000.00&completed_at=2026-10-02 12:03:45&deduct_amount=1012.00&fail_reason=收款账户户名不符&fee_amount=12.00&merchant_code=MCH10001&merchant_order_no=P20261002001&order_id=Ananpay20261002120000123a1b2c3d4&product_code=BANK_PAYOUT&status=4d7730fa42dd0c337736d389a97d236636e54e674fa9a2ee51035ee3893fe8fa9 V6 代收下单 amount=100.00&client_ip=203.0.113.50&merchant_code=MCH10001&merchant_order_no=T20260527001¬ify_url=https://merchant.com/callback&product_code=UPI_COLLECT&subject=会员充值×tamp=1748332800f5be0eca54150500d7b51b20ec783b3833efc65ddb5a5e730512b70816ef4aca V7 代收查单 merchant_code=MCH10001&order_id=COL20260527143000123abcd×tamp=174833286048a3b9e7f1a1739a46f2101921128d061b9deb8914c38a14e763cd9b65903595 V8 代收查余额 merchant_code=MCH10001×tamp=17483328604bd995ab430faafd97122a3b1ac1cec5603e9d8dedc273fc78f127aaf472f073 V9 代收成功通知 amount=100.00&completed_at=2026-05-27 14:35:22&created_at=2026-05-27 14:30:00&mch_fee_amount=3.00&merchant_code=MCH10001&merchant_order_no=T20260527001&order_id=COL20260527143000123abcd&product_code=UPI_COLLECT&real_amount=97.00&status=9453e1718048de8aaefb1d08ce7b0e926c69fe0fef79cf37b67a33edb7dc0b5d6
各语言实现 以下实现都严格遵循上面的规则(只签非空字符串、排除 sign / extra、按 key 升序)。在调试台生成请求后,还可以直接复制带完整请求代码的片段。
import javax . crypto . Mac ;
import javax . crypto . spec . SecretKeySpec ;
import java . nio . charset . StandardCharsets ;
import java . util . * ;
public static String sign ( Map < String , ? > params , String apiKey ) throws Exception {
TreeMap < String , String > sorted = new TreeMap <>();
for ( Map . Entry < String , ? > e : params . entrySet ()) {
String k = e . getKey ();
Object v = e . getValue ();
// 只取非空字符串;排除 sign、extra
if ( " sign " . equals ( k ) || " extra " . equals ( k ) || !( v instanceof String ) || (( String ) v ). isEmpty ()) continue ;
sorted . put ( k , ( String ) v );
}
StringJoiner payload = new StringJoiner ( " & " );
sorted . forEach (( k , v ) -> payload . add ( k + " = " + v ));
Mac mac = Mac . getInstance ( " HmacSHA256 " );
mac . init ( new SecretKeySpec ( apiKey . getBytes ( StandardCharsets . UTF_8 ), " HmacSHA256 " ));
byte [] hash = mac . doFinal ( payload . toString (). getBytes ( StandardCharsets . UTF_8 ));
StringBuilder hex = new StringBuilder ();
for ( byte b : hash ) hex . append ( String . format ( " %02x " , b ));
return hex . toString ();
} 复制 function pmgw_sign ( array $ params , string $ apiKey ): string {
$ data = [];
foreach ($ params as $ k => $ v ) {
// 只取非空字符串;排除 sign、extra
if ($ k === ' sign ' || $ k === ' extra ' || ! is_string ($ v ) || $ v === '' ) continue ;
$ data [( string ) $ k ] = $ v ;
}
ksort ($ data , SORT_STRING );
$ pairs = [];
foreach ($ data as $ k => $ v ) $ pairs [] = $ k . ' = ' . $ v ;
return hash_hmac ( ' sha256 ' , implode ( ' & ' , $ pairs ), $ apiKey );
} 复制 import hashlib , hmac
def sign ( params : dict , api_key : str ) -> str :
# 只取非空字符串;排除 sign、extra
items = sorted (( k , v ) for k , v in params . items ()
if k not in ( " sign " , " extra " ) and isinstance ( v , str ) and v != "" )
payload = " & " . join ( f " {k} = {v} " for k , v in items )
return hmac . new ( api_key . encode ( " utf-8 " ), payload . encode ( " utf-8 " ), hashlib . sha256 ). hexdigest () 复制 const crypto = require ( ' crypto ' );
function sign ( params , apiKey ) {
const payload = Object . keys ( params )
// 只取非空字符串;排除 sign、extra
. filter (( k ) => k !== ' sign ' && k !== ' extra ' && typeof params [ k ] === ' string ' && params [ k ] !== '' )
. sort ()
. map (( k ) => k + ' = ' + params [ k ])
. join ( ' & ' );
return crypto . createHmac ( ' sha256 ' , apiKey ). update ( payload , ' utf8 ' ). digest ( ' hex ' );
} 复制 import (
" crypto/hmac "
" crypto/sha256 "
" encoding/hex "
" sort "
" strings "
)
func Sign ( params map [ string ] any , apiKey string ) string {
keys := make ([] string , 0 , len ( params ))
for k , v := range params {
s , ok := v .( string ) // 只取非空字符串;排除 sign、extra
if k == " sign " || k == " extra " || ! ok || s == "" {
continue
}
keys = append ( keys , k )
}
sort . Strings ( keys )
parts := make ([] string , 0 , len ( keys ))
for _ , k := range keys {
parts = append ( parts , k + " = " + params [ k ].( string ))
}
mac := hmac . New ( sha256 . New , [] byte ( apiKey ))
mac . Write ([] byte ( strings . Join ( parts , " & " )))
return hex . EncodeToString ( mac . Sum ( nil ))
} 复制 通知验签 平台发出的异步通知同样携带 sign,商户收到后必须验签 :
取通知报文中的所有字段,按上面的规则筛选(排除 sign、extra、空值和非字符串值); 按同样的规则计算签名; 与通知中的 sign 比较(建议忽略大小写,并使用常量时间比较); 一致才处理业务,不一致则忽略该通知。 验签时请遍历报文中的所有字段 ,不要写死字段列表。平台在不同状态下发送的字段不同(例如代付失败通知才有 fail_reason),写死的列表会导致验签失败。
签名排查清单 遇到 签名验证失败 时,推荐直接用请求诊断 粘贴实际请求自动定位;也可以按下面的清单逐项排查:
密钥是否正确 :使用平台分配的 api_key(代收、代付相同),注意区分大小写和测试 / 生产环境;字段类型是否为字符串 :数字、布尔值不参与平台签名,所有字段请按字符串传;字段是否完整 :所有非空的字符串字段(除 sign、extra)都要参与签名,包括你多传的字段;排序是否正确 :严格按 key 的 ASCII 码升序;空值是否排除 :值为空字符串的字段不参与签名;sign 和 extra 是否排除 :这两个字段不参与签名;拼接格式是否正确 :key1=value1&key2=value2,值不做 URL 编码,首尾没有多余的 &;算法是否正确 :HmacSHA256,不是 SHA256、不是 MD5,也不要在待签名串末尾拼接 &key=...;输出格式是否正确 :小写十六进制字符串(64 个字符),不是 Base64;编码是否一致 :待签名字符串和密钥均使用 UTF-8(中文姓名、银行名尤其注意);签名值与发送值是否一致 :签名时用的值必须和实际发送的值完全相同(例如不要签 100、发 100.00);用测试向量自测 :用上面的测试向量和 demo_api_key_123456 计算,结果应与文档一致。