Webhook 设计与实现
问题
什么是 Webhook?如何安全可靠地实现 Webhook?
面试速答版
什么是 Webhook?和轮询有什么区别? Webhook 是「事件发生时服务方主动 POST 你」的回调机制:
- 轮询:客户端定时拉服务端问「有没有新数据」,浪费请求、有延迟。
- Webhook:服务端有事件就推过来,实时 + 省资源,典型如支付成功回调、GitHub Push 触发 CI。
- 对比 WebSocket:WebSocket 是长连接双向通信,Webhook 是短连接单向 HTTP 回调,不需要保持连接。
接收 Webhook 必须做哪几件事? 四个核心步骤,少一个都可能踩坑:
- 签名校验:用对方密钥对 raw body 算
HMAC-SHA256,比对请求头里的签名(如Stripe-Signature),防伪造。 - 幂等处理:同一个
event.id用 RedisSetNX去重,防止对方重试导致重复扣款。 - 快速返回 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 的可靠性?
答案:
- 签名校验:HMAC-SHA256 防止伪造
- 幂等处理:event ID 去重
- 重试机制:指数退避重试
- 超时控制:设置合理超时
- 日志记录:记录所有成功/失败的投递