在5分钟内构建生产就绪的Webhook投递系统
Source: Dev.to
工作原理:流程
关键特性
- ⚡ 即时响应(
202 Accepted) - 🔄 并行投递给所有订阅者
- 🔐 每个 webhook 都带有 HMAC SHA‑256 签名
- ♻️ 指数退避自动重试(1 秒、2 秒、4 秒)
- 📊 健康监控,连续 10 次失败后自动禁用
什么是外发 Webhook?
大多数开发者熟悉从 Stripe、GitHub、Slack 等服务接收 webhook。外发 webhook(也称为“反向 webhook”)让你的应用在事件发生时通知外部服务。当系统中发生某件事——用户注册、订单发货、支付完成——你的应用会向已注册的 webhook URL 发送 HTTP POST 请求。
这对于构建集成、实现实时通知以及让客户扩展你的平台至关重要。
挑战:正确构建并不容易
从零开始构建一个可靠的 webhook 投递系统需要解决许多复杂问题:
- URL 验证 – 确认 webhook URL 的合法性
- 安全性 – 使用 HMAC 签名防止篡改
- 可靠性 – 优雅地处理不可用的终端
- 可扩展性 – 在不阻塞的情况下向成千上万的订阅者投递
- 监控 – 投递统计、失败追踪、自动禁用失效 webhook
- 重试逻辑 – 指数退避、最大尝试次数、超时处理
基于队列的架构:秘密武器
可扩展的 webhook 投递关键在于将事件触发与投递解耦。
// 1. Store the event for audit trail
await conn.insertOne('events', eventData);
// 2. Mark all matching webhooks with the event ID
await conn.updateMany(
'webhooks',
{ status: 'active', $or: [{ events: eventType }, { events: '*' }] },
{ $set: { pendingEventId: eventData.id } }
);
// 3. Queue ALL webhooks in one atomic operation
await conn.enqueueFromQuery(
'webhooks',
{ status: 'active', pendingEventId: eventData.id },
'webhook-delivery'
);
魔法在于 enqueueFromQuery。它不是把 webhook 加载到内存再遍历,而是:
- 在一次数据库操作中将所有记录入队
- 处理成千上万的 webhook 而不会占用额外内存
- 立即返回(
202 Accepted) - 通过 worker 函数并行处理投递
这种架构即使面对海量订阅者列表也能实现即时响应。
安全第一:HMAC 签名
每个 webhook 负载都会附带加密签名,接收方可以据此验证真实性。
function generateSignature(payload, secret) {
const timestamp = Math.floor(Date.now() / 1000);
const sigBasestring = `${timestamp}.${payload}`;
const signature = crypto
.createHmac('sha256', secret)
.update(sigBasestring, 'utf8')
.digest('hex');
return { signature: `v1=${signature}`, timestamp };
}
每次 webhook 所发送的 Header
X-Webhook-Signature:HMAC SHA‑256 签名X-Webhook-Timestamp:Unix 时间戳(防止重放攻击)X-Webhook-Id:订阅 ID
接收方验证签名并拒绝超过 5 分钟的旧时间戳,以防止重放攻击。
自动 URL 验证
在接受 webhook 注册之前,系统会使用业界标准方法验证 URL:
- Stripe 风格验证 – 发送带有验证令牌的测试负载,期待返回 HTTP 200。
- Slack 风格挑战 – 发送随机挑战字符串,期待在响应中原样返回。
验证在 worker 队列中异步执行,注册请求会立即返回,而验证在后台进行。
智能重试逻辑
投递失败会触发带指数退避的自动重试。
{
"maxRetries": 3,
"backoffIntervals": ["1s", "2s", "4s"],
"timeout": "10s",
"autoDisableAfter": 10
}
一个 cron 任务每 30 分钟运行一次,重试已经失败超过一小时的 webhook,避免对失效端点进行猛烈请求。
事件灵活性
不同于某些系统只能使用预定义的事件类型,这套系统支持任意事件名称。
# E‑commerce events
POST /events/trigger/order.placed
# IoT events
POST /events/trigger/sensor.temperature.high
# Custom business events
POST /events/trigger/report.generated
订阅者可以为特定事件注册,也可以使用 "*" 通配符接收所有事件。
生产级监控
系统为每个 webhook 记录详细统计信息。
{
"deliveryCount": 1247,
"consecutiveFailures": 0,
"lastDeliveryAt": "2025-01-15T10:30:00Z",
"lastDeliveryStatus": "success",
"status": "active"
}
连续 10 次失败后,webhook 会自动被禁用以节约资源。用户可以通过 POST /webhooks/:id/retry 手动重试。
完整 API
提供完整的 CRUD API 用于 webhook 管理。
# Create webhook (with automatic verification)
POST /webhooks
{
"clientId": "customer-123",
"url": "https://example.com/webhook",
"events": ["order.placed", "order.shipped"],
"verificationType": "stripe"
}
# List webhooks
GET /webhooks?status=active&event=order.placed
# Trigger events (queues delivery to all subscribers)
POST /events/trigger/order.placed
{
"orderId": "123",
"total": 99.99,
"customer": "john@example.com"
}
# Check delivery stats
GET /webhooks/:id/stats
# Manually retry failed webhook
POST /webhooks/:id/retry
幂等注册
系统使用 clientId + url 作为复合键进行 upsert:
- 第一次注册会创建 webhook。
- 同一
clientId+url的后续注册会更新已有记录。 - 防止出现重复 webhook。
- 更新时保留 HMAC 密钥。
- 非常适合多租户 SaaS 应用。
实际集成案例
系统可以无缝集成到流行的工作流工具中:
- n8n – 创建 webhook 触发器,注册 URL,工作流自动激活。
- Zapier – 使用 “Webhooks by Zapier” 的 Catch Hook,注册时设置
verificationType: "stripe",你的 Zap 即可工作。 - 自定义应用 – 在任意语言实现签名验证,即可开始接收事件。
现场演示
想亲自试一试吗?完整实现已作为 Codehooks.io 模板提供。
