资讯文章 资讯 · NEWS
把条目状态推给对方:目录站的 Webhook 该怎么签、怎么重试
2026-10-24 约 1,424 字 预计阅读 4 分钟 yiyunkj
把
回调接口本质上是一个公开的 URL,谁都能往里发 POST。不加验证的话,任何人知道地址就能伪造一条"审核通过"的事件。
验证这件事有现成的规范可以照抄,不需要自己发明。

图片来源:网络,版权归原作者所有,侵权请联系删除
签名:三个头,签原始字节
目前比较通行的是 Standard Webhooks 那套,用三个请求头:webhook-id(事件唯一 ID)、webhook-timestamp(Unix 秒)、webhook-signature(形如 v1,签名)。
签名的计算方式是 HMAC-SHA256(密钥, "{id}.{timestamp}.{原始报文}"),三段用英文句点连接,结果做 base64。这四点上出错的最多:
- 必须签原始报文。把 JSON 解析再重新序列化,键顺序和空格一变,哈希就对不上了。
- 必须用常量时间比较。普通字符串相等比较会在第一个不同的字符处提前返回,这个时间差可以被用来逐字节试探。Python 用
hmac.compare_digest,Node 用crypto.timingSafeEqual。 - 必须校时间戳。签名只证明是谁发的,不证明是什么时候发的。容忍窗口通常取 5 分钟,超出直接拒绝。时间戳在签名串里面,攻击者改不动它。
- 必须留轮转通道。签名头支持空格分隔多个签名,换密钥期间新旧两把都验,等对方都切过去了再停用旧的。
顺便一提,X- 前缀按 RFC 6648 早就废弃了,新接口别再往这个方向走。
幂等:ID 要稳定,别用内容哈希
重试一定会发生,所以接收方必须能安全地收两次。发送方要做的是给每个逻辑事件一个稳定的 ID,每次重试都带同一个。接收方记住处理过的 ID,再见到直接返回 2xx 但不重复处理。
ID 不能用报文内容哈希生成——两次不同的业务事件可能生成完全相同的报文。而接收方的去重记录要保存得比最大重试窗口更久,否则一天前的事件重放过来还是会被当新的处理一遍。
重试节奏要写进文档
哪些情况重试、重试几次、间隔多久,都要在接口文档里写清楚,因为接收方要靠这个决定去重记录留多久。
| 响应 | 处理 | 理由 |
|---|---|---|
| 2xx | 成功,不再重试 | 对方已经收到 |
| 5xx、超时、网络错误 | 退避重试 | 对方暂时不可用 |
| 408、429 | 退避重试,尊重 Retry-After | 对方明确说了什么时候再来 |
| 其他 4xx | 不重试,进死信队列 | 对方拒绝了这份报文,重发也没用 |
退避用指数加抖动,典型节奏是 1 分钟、5 分钟、30 分钟、2 小时、12 小时、24 小时,六次左右收尾。全部失败之后进死信队列,同时给一个查询接口,让对方能自己查某次投递的状态并手动重放。
接收端的要求也要写清楚:几秒内返回 2xx,重活放到后面异步做。在自己的处理逻辑跑完才返回,很容易撞上发送方的超时,然后被判失败再重发一次。
对方填的 URL 是用户输入
这一条是发送方自己要防的:注册回调地址的是对方,他可以填一个内网地址,用你的投递服务去探测你的内网。这类服务端请求伪造的防护有三步——解析后屏蔽私有网段和链路本地地址(169.254.169.254 那个云元数据地址尤其要挡)、投递走独立的出口网段或代理、遇到重定向要重新校验一次目标地址,不然前面的检查会被一次 302 绕过。
报文内容也有取舍:塞完整资源方便对方,但一旦地址填错,数据就发到别人那里去了;只发一个 ID 让对方回来拉,安全些,代价是对方要多一次请求。涉及用户数据的事件建议用后者。
回调接口是给别人用的,文档里少写一句,对方就得多猜一次。把签名算法、重试节奏、幂等要求和超时约定写全,比事后在工单里解释划算得多。