微信支付 V3 回调验签:必须用 String 接收原始 body,以及那些绕不开的坑
先给结论:微信支付 V3 回调验签,核心是对原始请求体做 SHA256-RSA 校验。任何先解析成对象再验签的做法,都是在给自己埋雷。
为什么必须用 String 接收 body
验签的输入是「原始字节序列」。微信支付在服务端是对你收到的那个 HTTP body 原文计算的签名,所以你必须拿到逐字节一致的请求体。
- 用
String直接接收原始 body,是安全的。 - 用
@RequestBody接收 POJO/DTO,框架会先做反序列化,再重新序列化时字段顺序、空白符、转义都可能变化,签名必挂。 - 用
HttpServletRequest.getParameter()取不到 body,那是表单参数,不是原始请求体。
所以接口定义长这样:
@PostMapping("/notify")
public String notify(@RequestBody String rawBody, HttpServletRequest request) {
// rawBody 就是原始请求体,先验签,再处理
}
注意:这里不能用 @RequestBody MyNotifyDto dto 这种形式,也别用 @RequestBody byte[],直接用 String 就好,编码问题交给框架默认 UTF-8,微信支付文档明确是 UTF-8。
验签流程:拼字符串,用平台证书或微信支付公钥验
需要的数据
从请求头拿:
Wechatpay-SignatureWechatpay-NonceWechatpay-TimestampWechatpay-Serial(平台证书序列号,或微信支付公钥 ID)
从请求体拿:
- 整个原始 body,就是上面那个
rawBody
签名串
按这个格式拼接:
timestamp\n
nonce\n
body\n
注意末尾有个 \n,别丢。
验签用哪个密钥?
- 老商户:用 平台证书 的公钥,根据
Wechatpay-Serial找到对应证书。 - 新商户(2024 年 11 月起):改用 微信支付公钥,不再是平台证书。公钥 ID 以
PUB_KEY_ID_开头,从「商户平台 -> API安全」页面获取,保存成 PEM 格式配置。
代码思路:
String signature = request.getHeader("Wechatpay-Signature");
String nonce = request.getHeader("Wechatpay-Nonce");
String timestamp = request.getHeader("Wechatpay-Timestamp");
String serial = request.getHeader("Wechatpay-Serial");
String message = timestamp + "\n" + nonce + "\n" + rawBody + "\n";
// 根据 serial 找到对应公钥(平台证书或微信支付公钥)
PublicKey publicKey = getPublicKeyBySerial(serial);
// SHA256withRSA 验签
Signature sha256Rsa = Signature.getInstance("SHA256withRSA");
sha256Rsa.initVerify(publicKey);
sha256Rsa.update(message.getBytes(StandardCharsets.UTF_8));
boolean ok = sha256Rsa.verify(Base64.getDecoder().decode(signature));
验签失败直接返回失败状态,比如 4xx 或 5xx,微信会重发。不要返回 200。
解密 resource:先验签后解密
验签通过后,才能从 body 里解析出 resource 字段,然后解密 ciphertext。
- 解密算法:
AEAD_AES_256_GCM - 密钥:商户 API 私钥 + APIv3 密钥(32 字节)
- 需要的参数:
ciphertext:资源密文nonce:通知体里的resource.nonceassociated_data:通知体里的resource.associated_data(有的场景可能为空,也要按原值传)
流程必须是:
- 验签(用平台证书/微信支付公钥)
- 验签通过后,解析 body
- 用 API 私钥 + APIv3 密钥解密
resource.ciphertext
千万不要先解密再验签。签名是对整个原始 body 做的,不验签就解密,等于把业务逻辑暴露在不可信输入上,一旦有人伪造通知,解密可能直接抛异常,还会泄露确定性信息。
回调签名探测:WECHATPAY/SIGNTEST/ 前缀
微信支付官方会发起验签探测:用 Wechatpay-Signature 带 WECHATPAY/SIGNTEST/ 前缀的请求来测试你的验签实现是否正常。
很多同学遇到这种请求验签失败,于是“聪明”地特判跳过。千万不要特殊放行。微信支付就是用这个探测来确认你的验签逻辑没有问题,如果你放行了,探测结果反而判断你的实现有问题。正确做法:把这种探测流量当成正常回调,走标准验签流程,该失败就失败,该成功就成功。
其他必须注意的点
- 回调接口必须 5 秒内返回应答,超过 5 秒微信会认为超时并重发。
- notify_url 不能带参数,路径上的 query string 会被拒或导致回调失败。
- 必须公网 HTTPS 可访问,证书要合法,不能是自签名测试证书。
- 返回给微信的应答格式:成功返回
{"code":"SUCCESS"}之类,失败返回非 2xx 状态码,具体按微信支付文档要求,但核心是:验签失败、解密失败、业务处理失败都要返回失败状态,让微信重发。
一张图总结流程
收到回调(String rawBody)
→ 取请求头 Signature / Nonce / Timestamp / Serial
→ 拼 timestamp\nnonce\nbody\n
→ 用 Serial 对应公钥验签
→ 失败:返回 4xx/5xx
→ 成功:解析 JSON,取 resource
→ 用 APIv3 密钥 + 商户私钥解密 ciphertext(AEAD_AES_256_GCM)
→ 失败:返回失败状态
→ 成功:处理业务
→ 返回成功应答
核心就一句话:验签永远基于原始 body,不要碰任何二次解析后的对象。 至于公钥是用平台证书还是微信支付公钥,根据商户类型和 Wechatpay-Serial 决定,配置里把两种公钥都备好,按 ID 查就完了。