微信退款回调签名验证必须使用原始body,因gin.context.bindjson()会提前消耗请求体导致验签失败;正确做法是禁用自动解析,用io.readall(c.request.body)读取原始xml数据再手动解析。

微信退款回调的签名验证必须用原始 body
微信退款回调不走标准 JSON 解析流程,gin.Context.BindJSON() 会提前读取并消耗 c.Request.Body,导致后续无法获取原始字节流做签名验签——这是绝大多数人踩坑的第一步。
正确做法是:禁用自动解析,用 c.Request.Body 直接读取原始数据,再手动解析 XML(微信退款回调固定为 application/xml 格式)。
- 必须在
router.POST("/refund/notify", handler)前关闭gin.Mode()的默认 JSON 自动绑定(无需额外配置,但不能调用任何BindXXX()方法) - 用
io.ReadAll(c.Request.Body)获取原始字节,长度限制建议设为 10KB 内(微信回调体通常 - 验签前先检查
Content-Type是否为application/xml,避免被伪造请求绕过
验签时要用微信商户平台配置的 APIv3 密钥,不是 APPID 或 MCHID
微信退款回调签名验证依赖的是你在「微信商户平台 → 账户中心 → API 安全 → APIv3 密钥」里设置的 32 位字符串,不是 MCHID、也不是 APPID,更不是证书密码。
验签逻辑本质是:对原始 XML body 做 SHA256 with HMAC,密钥即 APIv3 密钥,结果转小写十六进制后与请求头 wechatpay-signature 比较。
-
wechatpay-nonce、wechatpay-timestamp、wechatpay-serial三个请求头必须一并参与签名计算(顺序不可错) - 签名原文拼接格式为:
timestamp + \n + nonce + \n + body(注意换行符是\n,不是\r\n) - 推荐直接使用官方 Go SDK 中的
wechatpay-go库的verifiers.NewWechatPayVerifier(),它已封装校验逻辑,比手写更可靠
成功响应必须返回纯文本 success,且 HTTP 状态码为 200
微信服务器收到非 200 状态码,或响应体含任何多余字符(比如空格、换行、JSON、XML),都会触发重试。常见错误是用 c.JSON(200, gin.H{"result": "success"}) —— 这会返回 JSON,微信直接判定失败。
- 必须用
c.String(200, "success"),确保响应体只有 7 字节success - 不能设置任何额外 Header(如
Content-Type: application/json),保持默认text/plain; charset=utf-8 - 响应前务必确认业务逻辑已提交事务(如更新订单状态、记账),否则先返回 success 再出错,会导致微信认为已成功而不再重试,造成资金状态不一致
保存回调数据前要加幂等性控制
微信退款回调可能重复投递(尤其网络抖动时),同一笔退款可能收到多条内容完全相同的回调。直接插入数据库会导致主键冲突或状态覆盖。
- 从 XML 提取
out_refund_no或transaction_id+out_trade_no组合作为唯一幂等键 - 建议用 Redis 缓存该键,TTL 设为 24 小时(微信回调重试窗口最长约 2 小时,留冗余)
- 入库前先查 DB 是否已存在该退款记录;若存在且状态非异常,则跳过处理,直接返回 success
- 不要依赖数据库唯一索引硬扛——失败时 Gin 默认返回 500,微信会持续重试,可能压垮服务
最易被忽略的是:验签通过后、业务处理前,必须把原始 body 和时间戳落库或发 MQ 备份。一旦后续环节出错,你得靠它人工对账,而不是靠微信重推——重推内容可能已变,或根本不再推送。











