gin中stripe webhook验证失败是因为默认不解析原始请求体,而签名验证需未解析的原始字节流;解决方法是禁用自动解析、手动读取并重置request.body。

为什么 Gin 里直接调用 Stripe SDK 会卡在 Webhook 验证失败?
因为 Gin 默认不解析原始请求体(raw body),而 Stripe Webhook 签名验证必须基于未解析的原始字节流。你用 c.Body() 或 c.BindJSON() 提前读取了 body,后续再传给 stripe.Webhook.ConstructEvent() 就会得到空或损坏的数据,最终报 signature verification failed。
解决方法只有一条:在 Webhook 路由注册时,**显式禁用 Gin 的自动 body 解析**,并手动读取一次 raw body:
- 用
c.Request.Body直接读取,注意要defer c.Request.Body.Close() - 读完后把 body 重新塞回
c.Request.Body(用io.NopCloser包装)——否则后续中间件或路由逻辑会收不到数据 - 把 raw body 字节传给
stripe.Webhook.ConstructEvent(),别传解析后的 map 或 struct
Gin 中如何安全处理 checkout.session.completed 事件?
这个事件表示用户已完成结账流程,但不等于支付成功 —— 它只代表 Stripe Checkout 页面关闭,可能用户点了“取消”、跳转失败、甚至没付款就关了页面。真正代表资金到账的是 invoice.paid 或 payment_intent.succeeded。
实操建议:
- 收到
checkout.session.completed后,**立刻调用stripe.Checkout.Session.Retrieve()拉取 session 最新状态**,检查payment_status字段是否为paid - 不要仅靠
mode(如subscription)判断业务逻辑,要查subscriptionID 是否真实存在且 active - 如果 session 返回
payment_status: "unpaid",说明用户没完成支付,此时不应激活服务或发邮件 - 对订阅类业务,更稳妥的做法是监听
customer.subscription.created+invoice.paid组合事件
如何避免 Gin + Stripe 在并发场景下重复处理同一事件?
Stripe 可能因网络问题重发 Webhook(尤其在超时或 5xx 响应时),Gin 若无幂等控制,就会多次执行扣库存、发邮件、开通会员等操作。
关键不是“拦截重复请求”,而是“确保同一事件 ID 只处理一次”:
- 从 Webhook event 对象中提取
id字段(如evt_1Pvxyz...),作为唯一键存入 Redis,设置 TTL 至少 24 小时 - 在处理逻辑最开头加判断:
if redis.Exists("stripe:event:" + evt.ID) { return } - 写入 Redis 必须在业务逻辑执行前,且用原子命令(
SETNX)防止竞态 - 不要依赖数据库主键冲突做幂等 —— Stripe 事件到达顺序不保证,
invoice.paid可能比checkout.session.completed先到
为什么本地调试 Webhook 总是 400 或签名无效?
绝大多数问题出在内网穿透和请求头丢失。Stripe Webhook 签名验证依赖两个 header:Stripe-Signature 和原始请求 method + path + body。任何中间层(如 ngrok、Cloudflare、Nginx)若过滤、改写或缓存这些 header,都会导致验证失败。
排查步骤:
- 用
curl -v直连你的本地服务,手动构造带Stripe-Signature的请求,确认服务本身能验签 - 检查内网穿透工具是否透传所有 header —— ngrok 默认透传,但某些免费版会 strip
Stripe-开头的 header - 确认 Gin 没启用 gzip 中间件(
gzip.Gzip()),它会修改原始 body 流 - Webhook secret 必须从 Stripe Dashboard 的 Webhooks 页面复制,不是 API Secret Key —— 错用会导致
invalid signature
复杂点在于:Stripe 的签名算法对换行、空格、header 大小写极其敏感,哪怕多一个空格或 header 名被转成小写,验证就崩。别信“看起来一样”,用 hexdump 看原始字节流才靠谱。











