微信、支付宝支付回调接口必须手动读取原始body验签,禁用bindjson/shouldbindxml;验签需严格按平台规则拼接原文,响应仅能返回纯文本“success”。

微信、支付宝的支付回调接口不能用 BindJSON 或 ShouldBindXML 直接解析,否则验签必败——因为这些方法会提前读空 c.Request.Body,导致后续拿不到原始字节流做签名比对。
必须禁用 Gin 自动绑定,手动读取原始 body
Gin 的 BindXXX 系列方法(包括 BindJSON、ShouldBindXML、Bind)内部会调用 c.Request.Body 读取并消耗请求体。而微信和支付宝验签都强制要求原始未解析的字节流,一旦被提前读过,io.ReadAll(c.Request.Body) 就会返回空或报错。
- 微信退款回调是
application/xml,必须用io.ReadAll(c.Request.Body)拿到原始 XML 字节,再用xml.Unmarshal解析 - 支付宝回调是
application/x-www-form-urlencoded,但不能用r.ParseForm(),它会自动 URL decode 值,而验签要求保留原始编码;应改用strings.Split(string(body), "&")手动拆解 - 所有回调 handler 路由必须绕过全局中间件(如 Logger、Recovery),避免中间件提前写响应头或 panic 后影响
c.Writer.WriteString("success") - 读取 body 前建议加长度限制,比如
http.MaxBytesReader(c.Writer, c.Request.Body, 10240),防恶意超大 payload
验签失败的三个高频原因
不是密钥填错,就是拼接逻辑或格式没对齐。微信和支付宝对签名原文的构造极其严格,差一个换行、少一个字段、大小写不一致都会失败。
- 微信回调验签:签名原文 =
timestamp + "\n" + nonce + "\n" + body(注意是\n,不是\r\n),密钥是商户平台「API 安全 → APIv3 密钥」里设置的 32 位字符串,不是证书私钥,也不是 MCHID - 支付宝验签:必须剔除
sign和sign_type字段,其余非空参数按键名升序拼成key1=value1&key2=value2,value 绝对不能 URL decode ——url.Values.Encode()会 encode,不能直接用 - 微信支付回调(非退款)验签要用平台公钥(
apiclient_cert.pem里的),解密用商户私钥(apiclient_key.pem,必须是以-----BEGIN RSA PRIVATE KEY-----开头的 PKCS#1 格式) - 别自己手写验签逻辑。微信推荐用官方
wechatpay-go库的verifiers.NewWechatPayVerifier();支付宝可用标准库crypto/rsa+crypto/sha256,更可控
响应必须是纯文本 "success",且仅此而已
微信和支付宝服务器收到非 200 状态码,或响应体含任何额外字符(空格、换行、JSON、HTML、BOM 头),都会判定为失败并持续重试。这不是容错设计,是硬性协议要求。
- 必须用
c.Status(http.StatusOK)设状态码,再用c.Writer.WriteString("success")写响应体 —— 不要用c.String(200, "success"),它会自动加Content-Type: text/plain; charset=utf-8和换行 - 绝对不要在 handler 里调用
c.JSON、c.XML、c.AbortWithStatusJSON等封装方法 - 业务逻辑(如更新订单状态、记账)必须在写响应前完成,并确认事务已提交;否则先返回 success 再出错,资金状态就不可逆地不一致了
- 日志只能打到
log.Printf或结构化 logger,禁止往c.Writer写任何东西(包括中间件注入 trace ID)
幂等控制不能只靠数据库唯一索引
微信和支付宝都明确说明回调可能重复投递(网络抖动、超时重试),仅靠数据库 UNIQUE 约束无法覆盖全部场景:插入失败时你得返回 success,但此时业务还没执行,下次重试又来,你就漏处理了。
- 推荐用 Redis
SETNX+EXPIRE控制,key 为"pay_callback:" + notify_id(微信)或"alipay_notify:" + notify_id(支付宝),TTL 设为 15–30 分钟 - state 参数防 CSRF 是另一回事,不能和幂等混用;
notify_id或微信的transaction_id才是真正用于去重的业务标识 - 即使用了 Redis,数据库层仍建议加
ON CONFLICT DO NOTHING(PostgreSQL)或INSERT IGNORE(MySQL)兜底,防止 Redis 故障时数据错乱
最易被忽略的是:回调 handler 必须独立注册,不走任何全局中间件链;body 读取、验签、业务更新、响应写出这四步之间不能穿插任何可能触发 WriteHeader 的操作;哪怕一行日志写错位置,整个回调流程就不可逆地进入重试地狱。











