Webhook 的工作原理是什么?如何保证安全、幂等和可靠重试?
简化版
Webhook 是一种“事件发生后由服务端主动回调业务方 URL”的集成方式。它的关键不是会发 HTTP 请求,而是要做好三件事:用签名和时间戳防伪造、防重放;用事件 ID 或业务单号做幂等;用超时、指数退避、死信和人工补偿保证可靠投递。
详细版
Webhook 常见于支付回调、代码仓库事件、告警通知、第三方 SaaS 集成。调用方在事件发生后向接收方配置的 URL 发送 HTTP POST,Body 里带事件类型和数据。接收方返回 2xx 表示成功,否则调用方会按策略重试。
典型流程:
第三方系统事件发生
|
v
签名 payload + timestamp
|
v
POST https://example.com/webhook
|
v
接收方验签、判重、入库、异步处理
设计 Webhook 时,高频考点集中在安全和可靠性。安全上要用 HMAC 签名、时间戳、nonce、HTTPS、IP 白名单等方式防止伪造和重放。可靠性上要把“收到回调”和“业务处理完成”解耦,先验签判重并落库,再异步消费。幂等上不能靠“我觉得第三方不会重复发”,必须用事件 ID、订单号或幂等表保证重复请求不会重复扣款、重复发货。
完整版教学
一、Webhook 解决的是什么问题
Webhook 的价值是把“你来问我有没有变化”变成“我有变化就通知你”。如果没有 Webhook,接收方只能轮询第三方接口,例如每 5 秒查一次订单状态。1 小时就是 720 次请求,订单真正变化可能只有 2 次,绝大部分请求都是浪费。
Webhook 把通知方向反过来:事件源主动 POST 到业务系统。它仍然基于 HTTP,所以实现简单、跨语言容易、能穿过大多数网关。它的难点在于网络不可靠、对方可能重复发、请求可能被伪造,不能把它当成普通内部接口。
| 模式 | 谁主动 | 优点 | 代价 |
|---|---|---|---|
| 轮询 | 接收方主动查 | 控制权在自己手里 | 空请求多,实时性差 |
| Webhook | 事件源主动推 | 实时、低浪费 | 要暴露公网入口并处理安全可靠性 |
二、一次标准 Webhook 请求应该包含什么
一个工程上比较完整的 Webhook 请求通常包含事件 ID、事件类型、发生时间、数据主体、签名和时间戳。事件 ID 用于幂等,事件类型用于路由处理,签名用于证明请求确实来自可信发送方。
POST /webhook/payment HTTP/1.1
Content-Type: application/json
X-Webhook-Timestamp: 1720000000
X-Webhook-Signature: sha256=8b3f...
{
"event_id": "evt_10086",
"event_type": "payment.succeeded",
"occurred_at": "2026-07-31T10:00:00Z",
"data": { "order_id": "O123", "amount": 19900 }
}
接收方不应该先执行业务再验签,也不应该只凭 URL 隐蔽性当安全措施。URL 泄露后,攻击者可以随便 POST 假事件;签名校验是第一道门。
三、签名校验为什么通常用 HMAC
HMAC 的思路是发送方和接收方共享一个 secret,发送方用 secret 对“时间戳 + 原始 Body”计算摘要,接收方用同样算法计算一次并比较。如果两边结果一致,说明请求内容没有被篡改,并且发送方大概率知道 secret。
signature = HMAC_SHA256(secret, timestamp + "." + raw_body)
注意这里要使用“原始 Body 字节串”,不要用 JSON 解析后再序列化的字符串。因为空格、字段顺序、转义方式变化都会导致签名不同。比较签名时也要使用常量时间比较,避免极端情况下的时序侧信道。
Webhook 验签的关键词是“原始 Body + 时间戳 + HMAC”,不是“接口地址够复杂所以安全”。
四、为什么要防重放攻击
签名只能证明请求曾经是合法发送方生成的,不能证明它是“这一次新生成的”。攻击者如果抓到一次合法请求,隔 10 分钟再原样发一遍,签名仍然可能正确。这就是重放攻击。
防重放常用两层:
| 措施 | 作用 | 示例 |
|---|---|---|
| 时间戳窗口 | 过旧请求直接拒绝 | 只接受 5 分钟内请求 |
| nonce / event_id 判重 | 同一请求不能重复处理 | evt_10086 已处理则返回成功 |
如果当前时间是 10:00,只接受 09:55 到 10:05 的时间戳,可以挡住大多数历史重放。再配合事件 ID 幂等,即使攻击者在窗口内重复发送,也不会重复执行业务。
五、幂等设计是 Webhook 的生命线
Webhook 发送方通常采用“至少一次投递”:只要没有收到 2xx,就会重试;有些发送方即使收到了 2xx,也可能因为网络抖动误判失败而重复发送。因此接收方必须把重复请求视为正常情况。
幂等做法通常是建立事件处理表:
CREATE TABLE webhook_events (
event_id VARCHAR(64) PRIMARY KEY,
event_type VARCHAR(64) NOT NULL,
status VARCHAR(16) NOT NULL,
received_at TIMESTAMP NOT NULL
);
处理时先尝试插入 event_id。插入成功表示第一次收到,可以继续处理;如果主键冲突,说明已经处理或正在处理,应直接返回 2xx 或查询状态。支付回调里还要以订单状态机兜底,例如订单已经是 PAID,再次收到支付成功事件不能重复加余额。
六、为什么要先落库再异步处理
很多人会在 Webhook HTTP 请求里直接执行业务逻辑,例如扣库存、发券、发邮件。这样做的问题是处理时间不可控:第三方通常要求 3 秒或 5 秒内响应,如果你的业务依赖数据库、消息队列、外部服务,很容易超时,导致对方重试。
更稳的流程是:
收到 Webhook
|
v
验签 + 时间戳 + 幂等插入
|
v
快速返回 2xx
|
v
后台任务异步消费事件并更新业务状态
这样 HTTP 回调入口只做轻量、确定、可快速完成的工作。真正复杂的业务在异步消费者里处理,失败后可以内部重试、告警或人工补偿。
七、重试策略要避免把对方打垮
Webhook 重试不能固定每 1 秒打一次,否则对方故障时会被重试流量压得更难恢复。更常见的是指数退避,例如 1 分钟、5 分钟、30 分钟、2 小时、12 小时,最多重试 10 次。每次请求要有超时,例如 3 秒连接超时、5 秒整体超时。
第 1 次失败 -> 1 分钟后重试
第 2 次失败 -> 5 分钟后重试
第 3 次失败 -> 30 分钟后重试
第 10 次仍失败 -> 进入死信队列
接收方也要准备好“补偿查询”。例如支付 Webhook 丢了,不能永远等通知;业务系统可以定时扫描待支付订单,主动调用支付平台查询最终状态。
八、常见误区与追问
- 误区:Webhook 就是普通 HTTP 回调,不需要专门设计。 它面对公网、不可靠网络和重复投递,必须考虑验签、幂等、重试和补偿。
- 误区:只要用了 HTTPS 就不用签名。 HTTPS 保护传输链路,签名用于确认请求来源和内容完整性,二者解决的问题不同。
- 误区:收到重复 Webhook 应该返回错误。 重复请求通常应按幂等处理,已处理成功可以直接返回 2xx,避免发送方继续重试。
- 误区:业务处理成功后再返回 2xx 最可靠。 如果业务耗时长,容易触发第三方超时重试;更稳的是验签判重落库后快速响应。
- 追问:如何防止重放攻击? 使用时间戳窗口、nonce 或 event_id 判重,并拒绝过旧请求。
- 追问:签名为什么要基于原始 Body? JSON 解析再序列化可能改变字节内容,导致签名验证不稳定。
- 追问:Webhook 丢了怎么办? 依靠发送方重试、接收方死信告警,以及业务侧主动查询补偿。
九、加强记忆
Webhook = 事件源主动 HTTP 回调,但面试真正考的是工程可靠性。安全靠 HTTPS 加 HMAC 签名、时间戳和防重放;正确性靠事件 ID、业务单号和状态机幂等;可靠性靠快速落库响应、异步处理、指数退避、死信和主动补偿。把这三层讲清楚,比只说“第三方回调接口”扎实得多。