微信公众号支付涉及两类签名:统一下单时的请求签名,以及前端调起支付时的JSAPI签名。很多排障文章只聚焦第一类签名,但实际报错往往出现在第二类。当你在后台看到“支付验证签名失败”或前端弹出“get_brand_wcpay_request:fail”时,意味着JSAPI参数的签名值与微信期望的不一致。一个容易被忽略的原因是:统一下单接口的签名算法支持MD5和HMAC-SHA256两种,业务方可以自由选择;但JSAPI调起支付的签名算法规定只能使用MD5。如果你为了提升安全性,在第二次签名时也改用SHA-256,就会直接失败。

要完整理解这个问题,需要先梳理微信支付签名的参数来源。统一下单成功后,微信会返回prepay_id。前端调起支付需要重新组装参数,包括appId、timeStamp、nonceStr、package、signType,并生成paySign。这里的signType如果填写“HMAC-SHA256”,微信客户端并不会按照SHA-256校验,而是仍然按MD5规则处理,导致两边计算出的字符串完全不同。更隐蔽的情况是,部分服务端SDK封装了统一的签名方法,在配置中把全局签名算法改成SHA-256后,两处签名都被替换,开发者在统一下单处测试通过就以为万事大吉,结果JSAPI拉起时必然报错。
JSAPI签名的参数排序与拼接规则
JSAPI签名要求将appId、timeStamp、nonceStr、package、signType五个参数按ASCII字典序从小到大排序,拼接成URL键值对格式,再在末尾加上商户API密钥key。注意这里的key不是AppSecret,而是商户平台上设置的APIv2密钥。例如参数排序后得到字符串:appId=wx1234567890&nonceStr=abc123&package=prepay_id=wx20170810123456&signType=MD5&timeStamp=1493443200。随后用MD5对该字符串求哈希,并将结果转为大写,这就是paySign。不要对package值做URL编码,prepay_id=后面的值保持原样即可。
实际编码时容易犯两个错误。一是把timeStamp写成时间戳字符串还是整型数字,签名时必须与前端传给微信的timeStamp完全一致,包括类型。二是nonceStr的生成方式,建议使用随机字符串,长度不超过32位,且不要包含特殊字符。以下Java代码演示正确的拼接过程:
import java.security.MessageDigest;
import java.util.Map;
import java.util.TreeMap;
public class JsapiSign {
public static String sign(Map<String, String> params, String apiKey) throws Exception {
Map<String, String> sorted = new TreeMap<>(params);
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : sorted.entrySet()) {
String k = entry.getKey();
String v = entry.getValue();
if (v != null && !v.isEmpty()) {
sb.append(k).append("=").append(v).append("&");
}
}
String stringA = sb.toString();
String stringSignTemp = stringA + "key=" + apiKey;
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(stringSignTemp.getBytes("UTF-8"));
StringBuilder hex = new StringBuilder();
for (byte b : digest) {
String s = Integer.toHexString(b & 0xff);
if (s.length() == 1) hex.append("0");
hex.append(s);
}
return hex.toString().toUpperCase();
}
}
这段代码使用TreeMap自动按字典序排序,空值参数不参与签名。如果你在参数中不小心加入了多余的字段,比如把appId写成appid,TreeMap排序结果会不同,签名自然会不一致。务必保证传给微信的参数列表与签名时完全一样。
对于PHP开发者,使用官方SDK时要注意配置项。在WxPayConfig中如果设置了签名类型为HMAC-SHA256,统一下单会使用SHA-256,但JsApiPay的GetParameters方法不会根据这个配置自动切换,它内部仍然调用MakeSign方法,而MakeSign默认使用MD5。如果你的框架对MakeSign做了全局重写,就会导致JSAPI签名错误。下面这段PHP代码展示了只对统一下单使用SHA-256、对JSAPI签名强制MD5的处理方式:
class WxPayConfig extends WxPayConfigInterface
{
public function GetSignType()
{
// 统一下单使用HMAC-SHA256
return 'HMAC-SHA256';
}
// 其他配置省略...
}
// 在生成JSAPI参数时,临时重置签名类型
$input = new WxPayUnifiedOrder();
$input->SetSignType('MD5'); // 强制JSAPI第二次签名用MD5
$result = WxPayApi::unifiedOrder($config, $input);
$jsApiParameters = $tools->GetJsApiParameters($result);
// $jsApiParameters内部已经包含paySign,此时签名算法为MD5
上述代码的关键在于第二次签名前显式调用SetSignType('MD5'),而不是依赖全局配置。这能避免因统一签名算法修改导致JSAPI签名悄然变化。很多开发者把SetSignType误以为是统一下单的签名类型,实际上它在调起支付参数生成阶段同样生效。
统一下单签名混淆是常见误判
有时候报错出现在统一下单阶段,开发者会误以为签名算法用错。统一下单确实支持MD5与HMAC-SHA256两种算法,但二者在拼接规则上有细微差别。使用HMAC-SHA256时,字符串拼接方式与MD5完全一致,只是最后一步哈希函数不同。不过如果你在微信商户平台配置了APIv3密钥,而仍使用APIv2签名接口,也可能导致签名失败。APIv3使用RSA签名和SHA256-RSA2048,与APIv2的MD5/HMAC-SHA256完全不同。检查你的商户平台是否已经升级到APIv3,如果代码里还在用旧的APIv2密钥,就会报“签名错误”。
另一个常见混淆是:开发者以为统一下单使用SHA-256签名后,JSAPI也必须用SHA-256,于是手动把signType字段设为HMAC-SHA256,并用SHA-256算法生成paySign。结果统一下单通过,前端拉起支付报签名错误。实际上微信对JSAPI的signType只接受MD5,即使你传了HMAC-SHA256,微信也不会按该算法校验。可以查看微信支付官方文档中关于JSAPI调起支付的参数说明,signType的取值只有MD5一种。因此,不要试图在JSAPI阶段提升哈希强度。
排查这类问题有个高效方法:使用微信支付官方提供的签名验证工具,将你的参数原文和签名结果粘贴进去,选择对应的签名算法,工具会明确告诉你签名是否正确。如果工具验证通过,说明你的算法没问题,问题出在参数一致性上,比如timeStamp精度、nonceStr不一致、或package值被转义。如果工具验证失败,优先检查哈希算法和密钥。切勿凭感觉猜测,工具能节省大量时间。
从日志中定位签名不一致的具体环节
后端调试时,建议把签名前的原始字符串、密钥、以及最终签名都打印到日志。注意不要记录完整密钥,只记录后四位或使用掩码,避免泄露。对比微信返回的错误信息,如果返回“签名错误”但没有更多细节,可以尝试用你自己计算的签名去替换前端SDK生成的paySign,观察是否成功。如果替换后成功,说明SDK内部签名逻辑有误;如果仍然失败,说明参数本身有问题。
前端调用微信JSAPI时,参数对象必须与后端签名参数完全一致。一个常见的隐蔽差异是timeStamp的格式。有些后端生成签名时使用字符串“1493443200”,而前端调用时传了数字1493443200,在微信客户端看来两个值不同。建议统一使用字符串类型,前端也以字符串传递。另一个差异点是nonceStr的大小写,某些语言生成的随机串包含大写字母,如果前端转成小写,就会导致签名不匹配。所以前后端最好使用同一套生成规则,或者后端直接返回完整参数对象给前端使用,前端不要做任何二次加工。
此外,package参数的格式要求是“prepay_id=xxx”,等号两边不能有空格,也不能写成“prepay_id= xxx”或“prepay_id =xxx”。有些开发者习惯在拼接时对URL参数进行编码,例如把等号编码成%3D,这会导致微信解析后的package值与签名时不一致。正确做法是package值保持字符串原样,不做URL编码。
当所有参数都确认无误时,再检查商户API密钥是否正确。APIv2密钥是32位小写字母数字组合,在商户平台的安全中心设置。如果不小心使用了AppSecret,或者把APIv3密钥填成了APIv2密钥,都会导致签名失败。密钥周围不要有空格、换行或不可见字符。建议将密钥放在配置中心统一管理,并在代码中去除首尾空白字符后再参与签名。
总结一下,微信公众号支付JSAPI签名错误的根因通常是第二次签名使用了非MD5算法,或者参数拼接与微信预期不一致。开发时可以遵循一个简单原则:统一下单可以自由选择MD5或SHA-256,但JSAPI签名一律使用MD5。通过日志比对和官方工具验证,可以快速定位是算法问题还是参数问题。希望这篇文章能帮你少走弯路,顺利拉起微信支付。