支付宝回调必须验签且需幂等处理;验签要用rsa2+sha256,公钥为纯pem格式,参数按升序拼接且不url decode;响应仅返回纯文本"success";幂等用redis setnx基于notify_id实现。

支付宝回调不能直接信任请求体里的字段,必须验签;否则攻击者伪造 out_trade_no 和 trade_status=TRADE_SUCCESS 就能白嫖订单。
支付宝回调必须用 RSA2 + SHA256 验签,不能跳过
支付宝异步通知是 POST 请求,Content-Type 为 application/x-www-form-urlencoded,但签名算法固定为 RSA2(即 SHA256withRSA),且公钥必须是 PEM 格式、不含头尾注释行(-----BEGIN PUBLIC KEY----- 等要删掉)。
- 验签前必须按文档规则预处理参数:剔除
sign和sign_type字段,其余非空参数按键名升序拼接成key1=value1&key2=value2形式,值不做 URL decode(Go 的url.Values.Encode()会自动 encode,不能直接用) - 推荐用标准库
crypto/rsa+crypto/sha256手动验签,避免第三方包隐藏逻辑细节或默认行为不一致 - 验签失败必须返回
"success"字符串(纯文本,无空格、无换行、无 HTML/JSON 包裹),否则支付宝会持续重试
别用 r.FormValue() 读支付宝回调 body
支付宝回调不是标准表单提交,r.ParseForm() 可能漏掉部分字段或破坏原始编码,尤其当参数含中文或特殊符号时。正确做法是直接读原始 body:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
-
body, _ := io.ReadAll(r.Body)获取原始字节流 - 用
url.ParseQuery(string(body))解析,再手动过滤sign和sign_type - 注意:
url.ParseQuery会对 value 做 URL decode,而支付宝验签要求“不 decode”,所以拼接签名原文时得用原始未 decode 的 value —— 建议先用strings.Split(string(body), "&")拆解,再逐个strings.SplitN(pair, "=", 2)提取 key/value,保留原始编码
Gin 路由里要禁用中间件干扰响应体
支付宝回调响应必须是干净的 "success",任何额外输出(日志、Header、中间件写入)都会导致验签失败或重试。
- 在 Gin 的回调 handler 中,不要调用
c.JSON()或c.String(),它们会自动加 Content-Type 和换行 - 用
c.Status(http.StatusOK)设置状态码,再用c.Writer.WriteString("success")直接写入 - 确保该路由路径(如
/alipay/notify)没被 Logger、Recovery 等中间件包裹;可单独注册:r.POST("/alipay/notify", alipayNotifyHandler),不套全局中间件
幂等性靠 Redis SETNX + EXPIRE,不是靠数据库唯一索引
支付宝可能因网络问题多次推送同一通知,仅靠 DB UNIQUE KEY(out_trade_no) 不够 —— 因为通知到达和 DB 写入之间存在时间差,重复请求仍可能并发写入。
- 收到通知后,第一时间用
redis.SetNX(ctx, "alipay:notify:"+notifyId, "1", 5*time.Minute)尝试加锁 - 失败则直接返回
"success"(已处理过) - 成功则执行业务逻辑(查单、改状态、发消息),完成后无需主动删 key,靠 TTL 自动过期
-
notifyId推荐用支付宝的notify_id字段(不是out_trade_no),它是每次通知的唯一标识
验签和幂等是两道独立防线,缺一不可;前者防伪造,后者防重放。很多人卡在验签失败却以为是参数顺序错了,其实是公钥格式不对或拼接时误 decode 了 value。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










