Skip to content
返回构建教程

Webhook 搭建

接收并可靠处理 webhook,带签名验证、幂等和死信队列。

为什么

Webhook 是世界告诉你的应用平台外发生了什么的方式——支付清算、仓库推送、文件上传。它们至少投递一次,意味着你会收到重复,且顺序不受你控。无幂等,重复 webhook 双重处理事件。无签名验证,攻击者可 POST 假 webhook 免费获订阅。无死信队列,瞬态 DB 错误导致 webhook 在提供方重试窗口过期后永久失败。这三个机制把 webhook 从可靠性负担变成可靠事件源。

何时使用

消费任何事件驱动外部服务时。健壮 handler 约 2-3 小时。仅轮询集成可跳过。

如何做

1. 暴露 POST 端点接受原始 body(非 JSON 解析)
   - 验签需精确字节;验证后再解析
   - 成功处理后立即返回 200,非之前

2. 用提供方 secret 和算法验签
   - Stripe:原始 body 的 HMAC-SHA256,用 timingSafeEqual 比较
   - GitHub:body 的 HMAC-SHA256,在 X-Hub-Signature-256 头

3. 查幂等:在 processed_events 表查事件 ID
   - 已见返回 200(提供方视 200 为成功,不重处理)
   - 新则处理后在同一 DB 事务里插事件 ID

4. 处理失败返回 5xx 让提供方重试——但限重试
   - 5 次失败后移事件到死信队列(DLQ)人工审查
   - 处理错误永不返回 4xx——4xx 意为"不重试"

5. 慢工作异步处理:入队 job,返回 200
   - webhook handler 的职责是确认,非做工作
   - 后台 worker 取 job;失败则 DLQ 接住

示例

import { createHmac, timingSafeEqual } from "crypto";

export async function POST(req: Request) {
  const raw = Buffer.from(await req.arrayBuffer());
  const sig = req.headers.get("x-hub-signature-256")!;
  const expected = "sha256=" + createHmac("sha256", process.env.WEBHOOK_SECRET!).update(raw).digest("hex");
  if (!timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    return new Response("签名无效", { status: 401 });
  }
  const event = JSON.parse(raw.toString());
  if (await isProcessed(event.id)) return new Response("ok", { status: 200 });  // 幂等
  await enqueueJob(event);  // 异步处理
  await markProcessed(event.id);
  return new Response("ok", { status: 200 });
}

常见问题

  • ⚠处理完成前返回 200,处理中崩溃永久丢事件。
  • ⚠用 === 而非 timingSafeEqual 比签名,启用对 secret 的时序攻击。
  • ⚠webhook handler 里同步处理,慢 DB 调用超时,提供方重试。
  • ⚠不处理乱序投递,事件 #2 在 #1 前处理,状态不一致。

最佳实践

  • ✓事件 ID + 处理时间戳存专用表带 30 天 TTL——旧重复不再到达。
  • ✓用带可见性超时的队列(SQS、BullMQ)同一事件不被两 worker 并发处理。
  • ✓加重放端点(仅管理员)从提供方重取最近 N 事件供人工重处理。
  • ✓DLQ 有 >0 项时告警——卡住的 webhook 是静默数据丢失 bug。