跳到主要内容

Webhook 设计与实现

问题

什么是 Webhook?如何安全可靠地实现 Webhook?

面试速答版

什么是 Webhook?和轮询有什么区别? Webhook 是「事件发生时服务方主动 POST 你」的回调机制:

  • 轮询:客户端定时拉服务端问「有没有新数据」,浪费请求、有延迟。
  • Webhook:服务端有事件就推过来,实时 + 省资源,典型如支付成功回调、GitHub Push 触发 CI。
  • 对比 WebSocket:WebSocket 是长连接双向通信,Webhook 是短连接单向 HTTP 回调,不需要保持连接

接收 Webhook 必须做哪几件事? 四个核心步骤,少一个都可能踩坑:

  • 签名校验:用对方密钥对 raw body 算 HMAC-SHA256,比对请求头里的签名(如 Stripe-Signature),防伪造
  • 幂等处理:同一个 event.id 用 Redis SetNX 去重,防止对方重试导致重复扣款。
  • 快速返回 200:Webhook 通常有超时(5-10s),收到后立即把任务丢队列异步处理,先返回 200。
  • HTTPS + IP 白名单:URL 必须 HTTPS,敏感场景再加 IP 白名单(如 Stripe 公布的 IP 段)。

发送 Webhook 怎么保证可靠?

  • 重试机制:失败按指数退避重试(如 1min/5min/30min/2h/10h),超过 N 次进死信队列。
  • 签名 + 时间戳:在 payload 里加 timestamp,接收方校验防重放(如 5 分钟内才有效)。
  • 管理后台:让用户能查看 Webhook 历史、手动重试、关闭/启用。

答案

Webhook 基本概念

Webhook 是一种基于 HTTP 回调的事件通知机制。当事件发生时,服务方主动向预先注册的 URL 发送 POST 请求。

接收 Webhook

webhook-receiver.ts
import crypto from 'crypto';
import express from 'express';

const app = express();

// 需要原始 body 做签名验证
app.use('/webhook', express.raw({ type: 'application/json' }));

app.post('/webhook/stripe', (req, res) => {
const signature = req.headers['stripe-signature'] as string;
const secret = process.env.STRIPE_WEBHOOK_SECRET!;

// 1. 签名校验(防伪造)
const expectedSig = crypto
.createHmac('sha256', secret)
.update(req.body)
.digest('hex');

if (signature !== `sha256=${expectedSig}`) {
return res.status(401).send('Invalid signature');
}

// 2. 解析事件
const event = JSON.parse(req.body.toString());

// 3. 幂等处理(防重复)
const processed = await redis.get(`webhook:${event.id}`);
if (processed) return res.status(200).send('Already processed');

// 4. 异步处理(快速返回 200)
await webhookQueue.add('process', event);
await redis.set(`webhook:${event.id}`, '1', 'EX', 86400);

res.status(200).send('OK');
});

发送 Webhook

webhook-sender.ts
interface WebhookConfig {
url: string;
secret: string;
events: string[];
}

class WebhookSender {
async send(config: WebhookConfig, event: string, data: unknown) {
const payload = JSON.stringify({ event, data, timestamp: Date.now() });

// 签名
const signature = crypto
.createHmac('sha256', config.secret)
.update(payload)
.digest('hex');

// 发送(带重试)
await this.sendWithRetry(config.url, payload, signature);
}

private async sendWithRetry(url: string, payload: string, sig: string, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature': `sha256=${sig}`,
},
body: payload,
signal: AbortSignal.timeout(10000),
});

if (res.ok) return;
} catch {
// 指数退避重试
await sleep(1000 * 2 ** i);
}
}
// 所有重试失败,记录日志
logger.error(`Webhook delivery failed: ${url}`);
}
}
注意

接收 Webhook 时必须快速返回 200,耗时逻辑放入队列异步处理。否则对方可能超时后重试,导致重复处理。


常见面试问题

Q1: Webhook 和轮询有什么区别?

答案

维度Webhook轮询
方向推送(服务方主动通知)拉取(客户端定时查询)
实时性实时取决于间隔
资源消耗低(事件触发)高(无效请求多)
可靠性需要重试机制简单可靠

Q2: 如何保证 Webhook 的可靠性?

答案

  1. 签名校验:HMAC-SHA256 防止伪造
  2. 幂等处理:event ID 去重
  3. 重试机制:指数退避重试
  4. 超时控制:设置合理超时
  5. 日志记录:记录所有成功/失败的投递

相关链接