联调微信支付JSAPI时,签名错误几乎是最常遇到的报错。一个容易踩的坑是:开发者把sign字段也丢进待签名串,结果每次计算都错。按照微信支付官方算法,sign是最终计算结果,不能参与它自身的生成过程;同时,JSAPI前后端参数对大小写极为敏感,timeStamp写成timestamp、nonceStr写成noncestr都会导致调起失败。本文先把统一下单签名和前端调起签名拆开,再解释sign不参与计算的原因,最后结合真实报错给出排查清单。

一、JSAPI支付里的两层签名不能混为一谈
微信公众号支付在接入过程中会经历两个关键签名阶段:统一下单签名和前端调起签名。统一下单发生在商户服务器调用微信支付下单接口时,请求参数包含appid、mch_id、nonce_str、body、out_trade_no、total_fee、spbill_create_ip、notify_url、trade_type、openid等。商户需要按照微信约定的规则生成sign,并随请求一起发送。微信服务器收到后,会按照同样的规则重新计算并比对,一旦不一致就返回签名错误。
前端调起支付阶段还需要一个paySign参数。它同样由商户服务器生成,参与签名的参数变为appId、timeStamp、nonceStr、package、signType。注意这里的参数名与统一下单阶段的下划线风格完全不同,采用驼峰格式,稍不留神就会写错。paySign本身不参与它自己的签名计算,这一点和统一下单阶段的sign字段规则完全一致。
下面是一段PHP生成统一下单签名的示例。代码中首先使用unset移除sign字段,避免混入待签名串,再按参数名ASCII码排序后拼接。
function buildSign(array $data, string $key): string
{
unset($data['sign']);
ksort($data);
$string = '';
foreach ($data as $k => $v) {
if ($v === '' || $v === null) {
continue;
}
$string .= $k . '=' . $v . '&';
}
$string .= 'key=' . $key;
return strtoupper(md5($string));
}
二、sign字段为何不参与签名生成:算法顺序决定结果
从逻辑上说,签名的作用就是为除签名本身以外的参数生成摘要。如果把sign放进待签名串,就会形成“计算sign需要sign”的自引用问题,这是永远无法收敛的。微信官方文档在签名算法第一步就明确要求:将请求参数中除去sign之外的参数,按照参数名ASCII码从小到大排序,再拼接成URL键值对格式,最后拼接商户API密钥,进行MD5或HMAC-SHA256运算。
在代码实现中,常见做法是先从一个Map或数组里执行remove('sign'),然后再排序、拼接、加密。这也解释了为什么验签时同样需要先剔除收到的sign字段,用剩余参数计算临时签名,再比对两个sign是否一致。如果验签时没有剔除sign,就会把旧签名当作业务参数参与新签名的计算,结果当然对不上。
这一规则并非微信支付独有。支付宝、银联等支付平台的签名体系中,sign也都是结果字段而不是输入字段。看到文档示例中有sign字段时,不要误以为它是普通业务参数。只要记住“签名结果不参与生成”,就能在反复报错时少走很多弯路。
三、大小写规则:参数名必须原样,签名结果建议统一大写
JSAPI支付参数名大小写非常敏感。后端统一下单参数全部使用小写加下划线,例如mch_id、nonce_str。而前端调起参数则采用驼峰格式,例如appId中字母I必须大写,timeStamp中字母S必须大写,nonceStr中字母S也必须大写。package必须固定为prepay_id=xxx的格式,不能写成PrepayId,也不能漏掉等号。
签名结果大小写也经常引发问题。微信支付MD5签名结果在官方示例和多数SDK中都是32位大写十六进制字符串。如果商户自行生成了小写结果,微信服务端比对时可能因大小写不同而拒签。建议在生成签名后统一使用strtoupper或toUpperCase转换为大写。同时,商户API密钥key本身也区分大小写,商户平台设置的key必须原样拼接,不能因为本地配置习惯而改动字母大小写。
下面这张表列出了前端调起阶段最容易写错的参数名,联调出现问题时可以优先对照检查。
| 容易写错 | 正确写法 | 说明 |
|---|---|---|
| timestamp | timeStamp | 前端调起参数,S必须大写 |
| noncestr | nonceStr | 前端调起参数,S必须大写 |
| appid | appId | 前端调起参数,I必须大写 |
| md5 | MD5 | signType取值建议使用大写 |
四、真实报错排查顺序与打印待签名串
常见报错包括统一下单返回“签名错误,请检查后再试”,以及前端调起支付时出现“支付验证签名失败”。此时不要急着怀疑算法实现,而是先把完整待签名串打印出来。把参与签名的参数按照ASCII排序后拼接起来,再与官方规则逐项对比,重点确认sign是否已经被移除、空值参数是否被过滤、参数名大小写是否和文档完全一致。
打印待签名串是一个成本很低但非常有效的排查手段。下面这段PHP代码可以在生成签名前把拼接好的字符串输出到日志,方便人工核对。
function buildSignForDebug(array $data, string $key): string
{
unset($data['sign']);
ksort($data);
$string = '';
foreach ($data as $k => $v) {
if ($v === '' || $v === null) {
continue;
}
$string .= $k . '=' . $v . '&';
}
$string .= 'key=' . $key;
error_log('待签名串: ' . $string);
return strtoupper(md5($string));
}
通过待签名串可以快速定位问题。例如total_fee必须是整数,不能带小数;notify_url中不要出现空格;如果是HMAC-SHA256签名,必须同时传入sign_type参数并保证算法一致。把sign移除、大小写核对、待签名串打印这三件事做好,JSAPI支付签名问题基本都能定位到根因。