Stripe Webhook 集成实战:完整接收、验签与事件处理代码
TL;DR 本文是 Stripe Webhook 的端到端集成指南:
- 5 分钟完成 Stripe Dashboard 配置 + Endpoint Secret 获取
- 掌握 Stripe 签名验证的完整流程(含 Go/Node.js 可运行代码)
- 获取 8 种核心事件的处理模板:支付成功、订阅创建、退款、 disputes 等
阅读收益
- ⭐⭐⭐ 高:直接对接全球最流行的支付平台
- 📖 难度:初中级,面向集成 Stripe 的后端 / 全栈开发者
- ⏱️ 约 14 分钟,含可运行代码 + Dashboard 截图说明
30 秒速览:Stripe Webhook 为何不可或缺
Stripe 的支付流程是异步的:用户完成支付后,状态不会立即同步到你的服务器。Webhook 是唯一可靠的方式获取支付结果。
用户支付 ──► Stripe 处理(3-10 秒)──► Webhook 推送到你的服务器
│
▼
┌───────────────┐
│ 更新订单状态 │
│ 发送确认邮件 │
│ 开通服务权限 │
└───────────────┘
⚠️ 绝对不能只依赖前端回调:用户可能关闭页面、网络中断,只有 Webhook 保证送达。
1. Stripe Dashboard 配置
1.1 获取 Endpoint Secret
# Step 1: 登录 Stripe Dashboard
open https://dashboard.stripe.com/webhooks
# Step 2: 点击 "Add endpoint"
# Step 3: 填写你的 Webhook URL
# https://api.yourapp.com/webhook/stripe
# Step 4: 选择需要监听的事件(Events to send)
# - invoice.paid ✅ 支付成功(最核心)
# - customer.subscription.created ✅ 订阅创建
# - customer.subscription.deleted ✅ 订阅取消
# - charge.refunded ✅ 退款
# - charge.dispute.created ✅ 争议
# Step 5: 保存后复制 "Signing secret"
# 格式:whsec_xxxxxxxxxxxxxxxx
1.2 测试环境配置
# Stripe 提供测试模式的独立 Webhook Endpoint
# Dashboard 切换开关:Test mode / Live mode
# 测试用信用卡号(Stripe 官方提供)
# 成功: 4242 4242 4242 4242
# 失败: 4000 0000 0000 0002
# 需要 3DS: 4000 0025 0000 3155
1.3 Stripe CLI 本地测试(无需部署)
# 安装 Stripe CLI
brew install stripe/stripe-cli/stripe
# 登录
stripe login
# 监听并转发到本地
stripe listen --forward-to localhost:8080/webhook/stripe
# 输出:
# Ready! Your webhook signing secret is whsec_xxx (^C to quit)
# 这个 secret 和 Dashboard 里的一致
💡 本地调试必备:Stripe CLI 会生成测试事件并本地转发,无需部署到服务器。
2. 签名验证:Stripe 特有格式
2.1 Stripe-Signature 格式解析
Stripe 的签名头格式与通用 Webhook 不同:
Stripe-Signature: t=1234567890,v1=abc123...,v0=def456...
t:时间戳(Unix epoch)v1:当前版本的签名(用这个)v0:旧版本签名(向后兼容,通常忽略)
签名计算方式:
signed_payload = timestamp + "." + request_body
signature = HMAC_SHA256(endpoint_secret, signed_payload)
⚠️ 关键点:签名是基于
timestamp + "." + body,不是仅 body。
2.2 Go 完整实现(含时间戳校验)
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"net/http"
"strconv"
"strings"
"time"
)
const stripeSecret = "whsec_xxxxxxxxxxxxxxxx" // 从 Dashboard 获取
func verifyStripeSignature(payload []byte, signatureHeader string) bool {
// Step 1: 解析签名头
parts := strings.Split(signatureHeader, ",")
var timestamp string
var signatures []string
for _, part := range parts {
kv := strings.SplitN(part, "=", 2)
if len(kv) != 2 {
continue
}
switch kv[0] {
case "t":
timestamp = kv[1]
case "v1":
signatures = append(signatures, kv[1])
}
}
if timestamp == "" || len(signatures) == 0 {
return false
}
// Step 2: 时间戳校验(防重放,5 分钟窗口)
ts, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil {
return false
}
if time.Now().Unix()-ts > 300 {
return false // 请求超过 5 分钟,拒绝
}
// Step 3: 计算签名
signedPayload := timestamp + "." + string(payload)
mac := hmac.New(sha256.New, []byte(stripeSecret))
mac.Write([]byte(signedPayload))
expected := hex.EncodeToString(mac.Sum(nil))
// Step 4: 对比签名(常量时间比较)
for _, sig := range signatures {
if hmac.Equal([]byte(sig), []byte(expected)) {
return true
}
}
return false
}
func stripeWebhookHandler(w http.ResponseWriter, r *http.Request) {
payload, _ := io.ReadAll(r.Body)
sig := r.Header.Get("Stripe-Signature")
if !verifyStripeSignature(payload, sig) {
http.Error(w, "Invalid signature", http.StatusBadRequest)
return
}
// 验签通过,解析事件
var event stripe.Event
if err := json.Unmarshal(payload, &event); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
fmt.Printf("Received event: %s (type: %s)\n", event.ID, event.Type)
w.WriteHeader(http.StatusOK)
}
Go 要点:
- Stripe 签名头含多个字段(
t、v1、v0),需逐个解析 signedPayload格式为timestamp + "." + body,这是 Stripe 特有的- 时间戳校验 + 签名验证双重防护
2.3 Node.js 完整实现
const stripe = require('stripe')('sk_test_xxx'); // 你的 API Key
const express = require('express');
const app = express();
const endpointSecret = 'whsec_xxx'; // 从 Dashboard 获取
app.post('/webhook/stripe', express.raw({type: 'application/json'}), (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
// Stripe 官方 SDK 提供内置验证
event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
} catch (err) {
console.log(`⚠️ Webhook signature verification failed: ${err.message}`);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
console.log(`✅ Received event: ${event.id} (${event.type})`);
// 处理事件
handleStripeEvent(event);
res.status(200).send('Received');
});
function handleStripeEvent(event) {
switch (event.type) {
case 'invoice.paid':
handleInvoicePaid(event.data.object);
break;
case 'customer.subscription.created':
handleSubscriptionCreated(event.data.object);
break;
// ... 更多事件
}
}
app.listen(8080, () => console.log('Webhook server running on port 8080'));
Node.js 要点:
express.raw()必须:Stripe SDK 需要原始 body bytes- 用 Stripe 官方 SDK 验签:
stripe.webhooks.constructEvent()自动处理时间戳 + 签名 - SDK 版本:
stripenpm 包 v8.0+
3. 8 种核心事件处理模板
3.1 事件处理总表
| 事件类型 | 触发时机 | 业务动作 | 优先级 |
|---|---|---|---|
invoice.paid | 发票支付成功 | 开通服务、发送确认邮件 | ⭐ 最高 |
invoice.payment_failed | 发票支付失败 | 提醒用户更新付款方式 | ⭐⭐ 高 |
customer.subscription.created | 订阅创建 | 创建账户记录、发送欢迎邮件 | ⭐⭐ 高 |
customer.subscription.updated | 订阅变更 | 更新套餐、记录变更历史 | ⭐⭐ 高 |
customer.subscription.deleted | 订阅取消 | 标记过期、发送挽留邮件 | ⭐⭐ 高 |
charge.refunded | 退款 | 返还权益、记录退款 | ⭐⭐ 高 |
charge.dispute.created | 争议/拒付 | 冻结账户、收集证据 | 🔴 紧急 |
setup_intent.succeeded | 支付方式设置成功 | 保存 payment_method_id | ⭐ 普通 |
3.2 invoice.paid — 支付成功(最核心)
func handleInvoicePaid(invoice *stripe.Invoice) error {
// 1. 幂等性检查
if isProcessed(invoice.ID) {
return nil
}
// 2. 获取关联信息
subscriptionID := invoice.Subscription.ID
customerID := invoice.Customer.ID
amountPaid := invoice.AmountPaid // 单位:分
// 3. 更新订单状态
if err := db.Exec(`
UPDATE orders
SET status = 'paid',
paid_at = NOW(),
stripe_subscription_id = $1,
amount_paid = $2
WHERE stripe_invoice_id = $3
`, subscriptionID, amountPaid, invoice.ID); err != nil {
return err
}
// 4. 开通服务(异步)
go func() {
provisionService(customerID, subscriptionID)
sendConfirmationEmail(customerID, invoice)
}()
// 5. 标记已处理
markProcessed(invoice.ID)
return nil
}
3.3 invoice.payment_failed — 支付失败
func handleInvoicePaymentFailed(invoice *stripe.Invoice) error {
customerID := invoice.Customer.ID
attemptCount := invoice.AttemptCount
// 1. 更新订单状态
db.Exec(`UPDATE orders SET status = 'payment_failed',
payment_attempts = $1 WHERE stripe_invoice_id = $2`,
attemptCount, invoice.ID)
// 2. 根据重试次数决定动作
if attemptCount == 1 {
// 第一次失败:发送友好提醒
sendPaymentReminder(customerID, invoice)
} else if attemptCount >= 3 {
// 多次失败:标记为逾期,可能暂停服务
suspendService(customerID)
sendUrgentNotice(customerID, invoice)
}
return nil
}
3.4 customer.subscription.deleted — 订阅取消
func handleSubscriptionDeleted(subscription *stripe.Subscription) error {
customerID := subscription.Customer.ID
// 1. 记录取消原因和时间
db.Exec(`UPDATE subscriptions
SET status = 'canceled',
canceled_at = NOW(),
cancel_at_period_end = $1
WHERE stripe_subscription_id = $2`,
subscription.CancelAtPeriodEnd, subscription.ID)
// 2. 如果是立即取消,立即停止服务
if !subscription.CancelAtPeriodEnd {
revokeServiceAccess(customerID)
sendFarewellEmail(customerID)
} else {
// 如果 cancel_at_period_end,服务维持到周期结束
scheduleServiceRevoke(customerID, subscription.CurrentPeriodEnd)
}
return nil
}
3.5 charge.dispute.created — 争议(紧急)
func handleDisputeCreated(dispute *stripe.Dispute) error {
chargeID := dispute.Charge.ID
amount := dispute.Amount
reason := dispute.Reason
// 1. 立即冻结相关资金
db.Exec(`UPDATE orders SET status = 'disputed',
dispute_reason = $1, disputed_at = NOW()
WHERE stripe_charge_id = $2`, reason, chargeID)
// 2. 暂停相关服务(防止继续消耗)
suspendService(dispute.Charge.Customer.ID)
// 3. 收集证据(异步)
go func() {
evidence := collectEvidence(chargeID)
submitDisputeEvidence(dispute.ID, evidence)
}()
// 4. 紧急告警
alertOpsTeam(fmt.Sprintf("Dispute created: %s, amount: %.2f", dispute.ID, float64(amount)/100))
return nil
}
4. Stripe Webhook 最佳实践
✅ Do’s
| # | 实践 | 原因 |
|---|---|---|
| 1 | 先返回 200,再异步处理 | Stripe 5 秒超时,复杂业务可能超时 |
| 2 | 所有事件处理幂等 | Stripe 可能重发,防止重复执行 |
| 3 | 记录所有接收事件的审计日志 | 对账、排查争议必需 |
| 4 | 用 Stripe SDK 验签 | 官方 SDK 持续更新,避免自己实现出错 |
| 5 | 区分 Test/Live Webhook Endpoint | 防止测试数据污染生产 |
| 6 | 配置多个事件类型,不要全选 | 减少不必要的推送和处理 |
❌ Don’ts
| # | 反模式 | 风险 |
|---|---|---|
| 1 | 只依赖前端回调确认支付 | 用户可能关闭页面,支付状态丢失 |
| 2 | 在 Webhook handler 中做同步 HTTP 调用 | 超时导致 Stripe 重试,可能重复处理 |
| 3 | 忽略 invoice.payment_failed | 用户付款失败后无感知,流失 |
| 4 | 将 stripe secret 提交到 Git | 被扫描工具发现后可被利用 |
| 5 | 处理所有事件类型但不区分 | 收到未处理的事件时 panic 或报错 |
5. 测试策略
5.1 Stripe CLI 触发测试事件
# 触发 invoice.paid 事件
stripe trigger invoice.paid
# 触发订阅创建
stripe trigger customer.subscription.created
# 触发退款
stripe trigger charge.refunded
# 查看转发日志
stripe listen --forward-to localhost:8080/webhook/stripe --print-json
5.2 单元测试(Go)
func TestHandleInvoicePaid(t *testing.T) {
invoice := &stripe.Invoice{
ID: "in_test_123",
AmountPaid: 2000,
Customer: &stripe.Customer{ID: "cus_test_123"},
Subscription: &stripe.Subscription{ID: "sub_test_123"},
}
err := handleInvoicePaid(invoice)
assert.NoError(t, err)
// 验证订单已更新
var status string
db.QueryRow("SELECT status FROM orders WHERE stripe_invoice_id = $1", invoice.ID).Scan(&status)
assert.Equal(t, "paid", status)
}
6. 常见问题排查
| # | 问题 | 排查方法 | 修复 |
|---|---|---|---|
| 1 | No signatures found matching the expected signature | 确认 endpoint secret 匹配环境(test/live) | 检查 Dashboard 环境开关 |
| 2 | Timestamp too old | 服务器时间是否准确? | 配置 NTP 同步,ntpd 或 chrony |
| 3 | Stripe 显示 “Timed out” | 处理时间 > 5 秒? | 先返回 200,异步处理 |
| 4 | 收到重复 invoice.paid | 幂等性检查未生效? | 验证 invoice.ID 去重逻辑 |
| 5 | Test 事件发到 Live Endpoint | Dashboard 配置错误? | 分别配置 test/live 两个 endpoint |
下一步
- 📖 Webhook 安全防御指南 →
- 📖 Webhook 重试与幂等性设计 →
- 📖 GitHub Webhook CI/CD 集成 →
- 📖 Webhook Gateway 设计 →
- 📖 Webhook 监控告警体系 →
本文全场约 4,000 词,提供 Stripe Dashboard 配置步骤、Go/Node.js 完整验签代码、8 种核心事件处理模板、Stripe CLI 测试命令以及 测试用信用卡号,可直接用于 Stripe 集成开发项目。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。
「saas」更多文章
短链接对 SEO 的影响与优化最佳实践
深度解析短链接对 SEO 的影响,覆盖 HTTP 重定向状态码对 PageRank 的传递差异、品牌短链与公共短链的 SEO 对比、Google 索引机制与实战优化建议,帮助 SEO 从业者和营销人员正确使用短链接。
UTM 参数 + 短链接:追踪每一条营销链路
本文系统讲解 UTM 参数的定义、5 个核心字段详解、命名规范,以及 UTM 与短链接结合的最佳实践。涵盖主流 UTM builder 工具对比、数据分析方法、常见错误规避和高级玩法,帮你建立一套完整的营销追踪工作流。
私域流量运营中的短链接策略:从引流到转化
深度解析短链接在微信、抖音、小红书等私域运营场景中的实战策略,涵盖渠道追踪、裂变增长、防封域名、活码技术、转化漏斗优化等核心方法论,帮助 SaaS 企业和品牌商家从引流到转化构建完整的私域增长闭环。