在5分钟内构建生产就绪的Webhook投递系统

发布: (2025年12月13日 GMT+8 21:08)
6 min read
原文: Dev.to

Source: Dev.to

工作原理:流程

Webhook delivery flow

关键特性

  • ⚡ 即时响应(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 模板提供。

Back to Blog

相关文章

阅读更多 »

如何文档化企业系统?

介绍 在现实世界中,极少有开发者像对待代码那样重视对系统进行文档化。没有文档,系统……