微信支付回调验签失败主因是私钥误用或格式错误:必须使用商户api证书私钥apiclient_key.pem(以-----begin rsa private key-----开头),而非apiv3密钥或平台证书;验签需用平台公钥解析header中wechatpay-signature等字段并与原始body拼接后验证。

微信支付回调验签失败,crypto/rsa 解密报错 crypto: invalid key
微信支付回调的验签和解密必须用商户私钥(不是 APIv3 私钥)+ 平台证书公钥配合完成。常见错误是把 APIv3 密钥当成商户私钥用,或误用 PEM 格式中的 -----BEGIN RSA PRIVATE KEY----- 和 -----BEGIN PRIVATE KEY----- 混淆。
- 确认你用的是「微信商户平台 → 账户中心 → API安全 → 商户API证书」下载的
apiclient_key.pem,且内容以-----BEGIN RSA PRIVATE KEY-----开头(不是-----BEGIN PRIVATE KEY-----) - Gin 中读取私钥时别直接
ioutil.ReadFile后传给rsa.PrivateKey;要用crypto/x509的ParsePKCS1PrivateKey解析 - 验签用的是微信回调 header 中的
WECHATPAY-SIGNATURE、WECHATPAY-TIMESTAMP、WECHATPAY-NONCE和原始 body 拼接后,用平台公钥(apiclient_cert.pem里的)验签
gin.Context.BindJSON 无法解析微信回调原始 XML 数据
微信支付回调发的是 application/xml,不是 JSON。Gin 默认不支持自动绑定 XML,BindJSON 强行调用会返回 invalid character ' 错误。
- 不要用
c.BindJSON(&req),改用c.ShouldBindXML(&req),且结构体字段需加xmltag - 微信回调 body 是纯 XML,没有外层封装,字段名大小写要完全匹配(如
return_code→ReturnCode string `xml:"return_code"`) - 若想统一处理,可提前用
c.Request.Body读一次原始数据,再手动解析,避免 Gin 自动绑定干扰
回调接口返回非 200 导致微信重复推送,但日志里看不到错误
微信支付回调要求 HTTP 状态码必须是 200,且响应 body 必须为纯字符串 success(不能带空格、换行、JSON 或 HTML)。Gin 默认返回 200 OK,但一旦中间件 panic 或 handler 提前 return,就可能变成 500 或 400,而 Gin 的 recovery 中间件默认不打印 panic 堆栈到 stdout。
- 在回调 handler 开头加
defer func() { if r := recover(); r != nil { log.Printf("panic in wechat callback: %+v", r) } }() - 显式写
c.String(200, "success"),不要用c.JSON或c.Status单独设状态码 - 微信服务端对响应超时非常敏感(通常 5s),数据库操作、远程调用务必加 context timeout,否则超时后微信会重试
如何安全地校验 out_trade_no 并防止重复通知
微信可能因网络问题多次推送同一笔订单的回调,仅靠验签不能防重放。必须结合业务唯一 ID(out_trade_no)做幂等控制,但直接查库 + 更新容易出现并发冲突。
- 推荐用 Redis + Lua 做原子性判断:
SETNX wechat:notify:${out_trade_no} 1 EX 3600,成功才继续处理,失败直接返回 success - 数据库更新时 WHERE 条件必须包含当前状态(如
status = 'unpaid'),避免已处理订单被二次更新 - 微信回调不保证顺序,同一订单的「支付成功」和「退款通知」可能交错到达,
transaction_id和out_trade_no都要参与幂等键设计
微信回调验签和解密逻辑耦合度高,建议抽成独立函数,参数明确区分「原始 body」「header map」「商户私钥」「平台公钥」,避免在 handler 里混写 crypto 代码。平台证书过期、私钥权限错误、时钟偏差超过 9 分钟都会导致验签失败,这些点线上必须监控。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











