微信支付调起报「支付验证签名失败」:paySign 的两种构造与 8 条排查顺序
1. 先确认原因:paySign 与下单签名不通用
小程序 wx.requestPayment 里的 paySign 是调起支付签名,并非下单接口返回的 sign(V2),也并非下单请求时用的 signature(V3)。两者的参数集、算法、编码方式都不同:
- V2:按参数名 ASCII 字典序拼串,MD5 或 HMAC-SHA256,结果转大写。
- V3:固定四行签名串,SHA256 with RSA 私钥签名,结果 Base64。
因此,如果「支付验证签名失败」,大部分情况是拿错了签名、算错了算法,或者签名串与原请求参数不一致。
2. 两种签名的可复现构造
V3 小程序调起支付
签名串四行,每行一个参数,行尾以 \n(0x0A)结束,包括最后一行;参数本身以 \n 结尾时需再补一个 \n。
1 appid
2 timeStamp
3 nonceStr
4 package(prepay_id=xxx)
示例待签名串:
wx8888888888888888
1414561699
5K8264ILTKCH16CQ2502SI8ZNMTM67VS
prepay_id=wx201410272009395522657a690389285100
用商户私钥做 SHA256 with RSA 签名,结果 Base64 编码即 paySign:
$ echo -n -e "wx8888888888888888\n1414561699\n5K8264ILTKCH16CQ2502SI8ZNMTM67VS\nprepay_id=wx201410272009395522657a690389285100\n" | openssl dgst -sha256 -sign apiclient_key.pem | openssl base64 -A
signType 不参与签名,但必须传递,默认 RSA,签名值通过 paySign 字段传给前端。
App 支付 V3 的签名串与此不同:appid\ntimestamp\nnoncestr\nprepayid\n,字段名是小写 timestamp、noncestr,prepayid 不带 prepay_id= 前缀。小程序侧照抄 App 的拼法会直接验签失败。
V2 旧版调起支付
参与签名的参数为 appId、timeStamp、nonceStr、package、signType,按参数名 ASCII 字典序排序,用 URL 键值对格式拼成 stringA,末尾拼 &key=APIv2密钥,MD5 后全部转大写。signType 为 HMAC-SHA256 时改用 hash_hmac("sha256"),同样转大写。
用上面的示例参数,stringA 形如:
appId=wx8888888888888888&nonceStr=5K8264ILTKCH16CQ2502SI8ZNMTM67VS&package=prepay_id=wx201410272009395522657a690389285100&signType=MD5&timeStamp=1414561699
再拼 &key=xxx 做 MD5、转大写,得到 paySign。注意:签名原串必须用参数原值,不能是 URLencode 之后的值;空值不参与签名;参数名区分大小写;验签时 sign 本身不参与签名;微信接口可能新增字段,验签实现要能容纳扩展字段。
3. 字段在 V2 / V3 下的差异
| 字段 | V2(旧版) | V3(小程序) |
|---|---|---|
appId | 参与签名,键名 appId(大写 I) | 签名串第 1 行,取实际调起支付小程序的 appid,须为下单传入的 sp_appid 或 sub_appid 之一 |
timeStamp | 参与签名,标准北京时间起秒数 | 签名串第 2 行;系统取到毫秒的要转成 10 位秒 |
nonceStr | 参与签名,不长于 32 位 | 签名串第 3 行,不长于 32 位 |
package | 参与签名,格式 prepay_id=xxx | 签名串第 4 行,格式 prepay_id=xxx |
signType | 参与签名,MD5(默认)或 HMAC-SHA256 | 传 RSA,不参与签名 |
paySign | MD5/HMAC-SHA256 后转大写 | SHA256 with RSA 后 Base64 |
H5 在微信浏览器内用 WeixinJSBridge 调起支付时,字段同样为 appId、timeStamp、nonceStr、package、signType、paySign,参数名大小写敏感,签名串同样是四行(公众号 id / 时间戳 / 随机串 / 订单详情扩展字符串),返回 get_brand_wcpay_request:ok / :cancel / :fail。
4. 排查清单:先区分 V2/V3,再逐项对
先确认下单接口与调起实现属于同一套体系,再按下面顺序检查。每项都先看现象,再看验证方式。
1)签名类型与下单时不一致。 现象:下单用 HMAC-SHA256,调起按 MD5 算 paySign。验证:核对下单请求/返回中的 signType,与实际计算 paySign 使用的算法是否一致;HMAC-SHA256 场景需 hash_hmac("sha256") 后再转大写。
2)原串参数与实际请求参数不一致。 现象:时间戳、随机串中途重新生成过,只同步了一边。验证:把真正传给 wx.requestPayment 的四个字段按行打印,与计算签名时使用的串逐行 diff。
3)参数名大小写不正确。 现象:写成 appid、timestamp、noncestr 之类 lowercase 形式。参数名大小写敏感,写错直接验签失败。验证:对照字段表逐字比对;H5 WeixinJSBridge 同样如此。
4)API 版本不一致。 现象:下单走 V2,调起按 V3 实现,或反过来。验证:确认下单与调起是否配套。两者差异不止签名:参数格式 XML vs JSON,签名 MD5/HMAC-SHA256 vs 非对称 SHA256-RSA,回调加密无 vs AES-256-GCM。
5)下单与调起使用的商户号信息不一致。 现象:A 商户号下单,用 B 商户号的私钥或密钥签名。验证:核对下单请求里的商户号与 apiclient_key.pem / APIv2 密钥所属商户号是否一致。
6)key 不正确。 现象:V2 下按规则拼完,MD5 结果始终对不上。验证:确认用的是 APIv2 密钥(32 字节),路径为商户平台 → 账户中心 → 账户设置 → API 安全 → 设置 API 密钥。修改后新旧两份密钥并存,新密钥立即生效、旧密钥 15 天后自动失效,旧密钥未作废时不能再次修改;同时确认各环境配置的是哪一份。
7)没有重新生成签名,直接复用下单结果。 现象:把统一下单返回的 sign(V2)或下单请求的 signature(V3)直接填进 paySign。验证:检查 paySign 是否由调起参数重新计算;V3 下单签名串与调起签名串的参数集并不相同。
8)签名串拼接细节出错。 V3 现象:换行多一个或少一个,常见于用 echo 不带 -n 导致尾部多出换行,或最后一行漏掉 \n。验证:把待签名串写入文件,用 od -c / xxd 看行尾字节是否为 0x0A;命令行复现时使用 echo -n -e。V2 现象:空值被拼进串、原串用了 URLencode 后的值。验证:检查 stringA 是否只含非空参数、值是否为参数原值。
5. 服务商模式:sp_appid / sub_appid 一致性
V3 调起支付的 appId 必须使用实际调起支付小程序的 appid,且必须是下单时传入的 sp_appid 或 sub_appid 之一。微信支付会校验下单与调起支付所用 appid 的一致性,两者不匹配时报签名失败。
这一条容易漏,是因为它不像算法错误那样明显:签名串本身拼对了,openssl 也能算出结果,但签名里放的 appid 与下单时的 appid 不是同一个主体,校验依然不通过。排查时把下单请求里的 sp_appid、sub_appid 与前端 wx.requestPayment 的 appId 并排贴出来比对。
6. 前端回调不能作为订单状态依据
wx.requestPayment 的 errMsg 有三种:
requestPayment:ok:调用支付成功;requestPayment:fail cancel:用户取消;requestPayment:fail (detail message):后台返回详细失败原因。
若出现 "errno": 102, "errMsg": "requestPayment:fail jsapi has no permission",说明支付功能权限被公众平台限制。
前端回调并不保证可靠,不能只依赖它判断订单状态。订单状态以后端查询订单结果与支付成功回调通知为准。