payment_intent.succeeded事件收不到主因是gin中间件提前读取请求体导致验签失败;必须确保原始io.readcloser未被消费,禁用默认logger、单独注册webhook路由、用teereader安全复制body。

为什么 payment_intent.succeeded 事件收不到?
Webhook 验签失败是 Gin 中最常导致事件丢失的原因,不是密钥错了,而是请求体被提前读取了。Gin 默认中间件(比如 gin.Logger()、gin.Recovery() 或自定义日志/鉴权逻辑)会调用 c.Request.Body 的 ReadAll 或 json.NewDecoder,导致后续 stripe.Webhook.ConstructEvent 拿到空 body。
必须确保原始 io.ReadCloser 未被消费:
- 在路由注册前加
gin.SetMode(gin.ReleaseMode)关闭默认 Logger(或手动移除) - Webhook 路由单独注册,不走通用中间件链
- 用
c.Request.Body原始句柄传给stripe.Webhook.ConstructEvent,别先 decode 成 struct - 若需记录原始 body,用
io.TeeReader+bytes.Buffer复制一份,再传给验签函数
如何安全生成 PaymentIntent 并返回 client_secret?
前端要渲染信用卡输入框,必须拿到 client_secret;但这个值绝不能硬编码或从 URL query 传,必须由后端动态创建并返回。关键点在于金额单位、幂等性和上下文绑定:
-
Amount必须是整数,单位为「最小货币单位」——例如 USD 是分,1999表示 $19.99,不是19.99 - 务必设置
IdempotencyKey(推荐用 UUID v4),否则网络重试可能造成重复创建 intent,甚至重复扣款 - 用
Metadata绑定业务订单 ID(如"order_id": "ord_abc123"),后续 Webhook 处理时可快速关联 - 不要把
secret_key写死在代码里,用os.Getenv("STRIPE_SECRET_KEY")读取,且只在测试/生产环境切换密钥前缀(sk_test_/sk_live_)
示例返回结构(JSON):
{"client_secret": "pi_xxx_secret_yyy", "payment_intent_id": "pi_xxx"}
前端调用 confirmCardPayment 后,后端怎么确认真的付成功了?
别信前端传来的任何 status 字段或 success 回调——攻击者可以伪造请求绕过校验。唯一可信路径是监听 Webhook 事件并严格验签:
- 只信任
payment_intent.succeeded事件,忽略requires_action或processing状态 - 验签必须用 Dashboard 里 Webhooks 页面生成的
Signing Secret(格式类似whsec_xxx),不是secret_key - 收到事件后,先调用
stripe.Webhook.ConstructEvent,再检查event.Type和event.Data.Object.Status == "succeeded" - 最后核对
event.Data.Object.Amount与业务订单金额是否一致,防止金额被篡改
漏掉其中任一环,都可能导致发货错误或资金损失。
Gin 中处理 3D Secure 流程要注意什么?
当用户卡支持 3D Secure(如多数欧洲 Visa/Mastercard),confirmCardPayment 会返回 error: {code: "authentication_required"},此时前端需调用 handleCardAction 弹出银行验证页。后端无需主动干预,但必须:
- 允许
payment_intent.requires_action事件进入 Webhook,并记录状态,避免误判为失败 - 前端跳转验证页后,Stripe 会自动重试 confirm,最终触发
succeeded或payment_failed - 别在
requires_action状态就更新订单为“已支付”——它只是中间态,银行还没放行资金 - 如果用户关闭验证页,intent 会在 24 小时后自动变为
canceled,需监听该事件清理订单
整个 3D Secure 流程是异步的,Gin 后端唯一要做的就是耐心等 Webhook,而不是试图同步等待结果。











