案例 微信支付 V3 回调验签:必须用 String 接收原始 body

2026-08-30 21:04:54

微信支付 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-Signature
  • Wechatpay-Nonce
  • Wechatpay-Timestamp
  • Wechatpay-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));

验签失败直接返回失败状态,比如 4xx5xx,微信会重发。不要返回 200。

解密 resource:先验签后解密

验签通过后,才能从 body 里解析出 resource 字段,然后解密 ciphertext

  • 解密算法:AEAD_AES_256_GCM
  • 密钥:商户 API 私钥 + APIv3 密钥(32 字节)
  • 需要的参数:
    • ciphertext:资源密文
    • nonce:通知体里的 resource.nonce
    • associated_data:通知体里的 resource.associated_data(有的场景可能为空,也要按原值传)

流程必须是:

  1. 验签(用平台证书/微信支付公钥)
  2. 验签通过后,解析 body
  3. 用 API 私钥 + APIv3 密钥解密 resource.ciphertext

千万不要先解密再验签。签名是对整个原始 body 做的,不验签就解密,等于把业务逻辑暴露在不可信输入上,一旦有人伪造通知,解密可能直接抛异常,还会泄露确定性信息。

回调签名探测:WECHATPAY/SIGNTEST/ 前缀

微信支付官方会发起验签探测:用 Wechatpay-SignatureWECHATPAY/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 查就完了。

复制全文 生成海报 微信支付 回调 验签 接口 安全

推荐文章

程序员茄子在线接单