Webhook入门与调用流程
Webhook 到底是什么,用一通“送达电话”就能讲明白 Webhook 到底是什么,用一通送达电话讲明白 Webhook 到底是什么,用一通送达电话讲明白 今天做前后端项目时,如果第三方平台让你填写“Webhook 地址”或者“回调地址”,第一次接触确实容易迷糊。
这个地址由谁调用?前端要不要处理?为什么支付成功以后,平台还要再请求一次我们的服务器?
我把 GitHub 和 Stripe 的官方文档放在一起看了一遍,两家的业务差别很大,Webhook 的工作方式却很接近。
你先把一个网址交给对方,同时告诉对方自己关心哪些事情。等这些事情发生以后,对方会向这个网址发送一个 HTTP 请求,把发生了什么一并告诉你。
这个由对方主动发来的通知,就是 Webhook。
先从一份外卖说起 你点了一份外卖,想知道骑手什么时候送到。
一种办法是每隔一分钟打一次电话。
“到了吗?”
“还没有。”
过一分钟再打。
“现在到了吗?”
这很像程序里的轮询。你的系统隔一段时间就请求一次接口,反复查询结果。
另一种办法省事得多。你留下手机号,骑手送到以后主动给你打电话。
这个“送到以后主动打电话”的动作,就很像 Webhook。
在这个例子里,各个角色可以这样对应。
外卖场景 程序里的角色 你的手机号 Webhook 地址 骑手 发送通知的第三方平台 外卖已经送到 发生的事件 骑手主动打电话 发送 Webhook 请求 你回复“知道了” 返回 HTTP 2xx 状态码 把两种方式画出来对比,差别更清楚。
外卖平台 你(点外卖的人) 外卖平台 你(点外卖的人) 轮询:每隔一分钟打一次电话 loop [每隔一分钟] Webhook:留电话,送到主动打给你 到了吗? 还没有 留下电话,送到通知我 好的 外卖到了,主动打来 知道了 这里有一个很重要的区别。
普通 API 请求由你的系统先开口。你的系统想查订单,便主动调用订单接口。
Webhook 由对方系统先开口。支付成功、代码提交或者物流状态变化以后,对方向你事先留下的网址发送通知。
GitHub 对 Webhook 的介绍也是这个意思。开发者订阅某些事件以后,GitHub 会在事件发生时,向指定网址发送带有事件数据的 HTTP 请求。GitHub Webhook 说明
为什么已经有 API,还需要 Webhook 有些事情可以立即得到结果。
例如用户查询商品详情,前端发出请求,后端马上返回商品名称和价格。整个过程通常几百毫秒就结束了。
还有些事情需要等待。
用户提交支付后,银行可能还在处理。视频上传以后,服务器还要转码。代码推送到 GitHub 后,测试任务也要跑一段时间。
你的系统很难一直等着,也没有必要每秒查询一次。
Webhook 适合处理这种稍后才有结果的事情。你的系统先提交任务,第三方平台完成处理以后,再主动通知你的服务器。
两者最直观的区别,是谁先发起请求。
Webhook:对方主动通知 事件发生后请求
第三方平台 你的系统 普通 API:你的系统主动问 主动请求
返回结果
你的系统 第三方平台 Stripe 的官方文档提到,Webhook 可以接收银行确认付款、用户发起争议和订阅扣款成功等异步事件。平台会通过 HTTPS 把 JSON 数据发送到商家登记的地址。Stripe Webhook 文档
前端、后端和支付平台怎样联系起来 拿网上付款举例,整个过程可以画成下面这张图。
用户点击支付 前端请求创建订单 业务后端保存待支付订单 业务后端请求支付平台 用户完成付款 支付平台发送 Webhook 业务后端验签并更新订单 前端查询订单状态 页面显示支付成功 每一步都在处理自己的事情。
前端负责收集用户操作和显示结果。
业务后端负责创建订单、保存订单状态,并向支付平台发起支付请求。
支付平台负责收钱。付款结果确定以后,它会调用业务后端提供的 Webhook 地址。
业务后端收到通知,确认这条通知可信,然后把订单从“待支付”改成“已支付”。
前端随后查询自己的业务后端,看到订单已经支付,再更新页面。
订单编号把这些请求串在一起。前端创建的是 1001 号订单,后端向支付平台提交的也是 1001,支付平台发回通知时仍然要带上这个编号。后端拿到编号,才能知道该修改哪一笔订单。
换成时间顺序看,整条链路的调用是这样。
支付平台 后端 前端 支付平台 后端 前端 创建订单 1001 发起支付 打开收银台 完成付款 Webhook 通知支付成功 验签并更新订单为已支付 查询订单 1001 状态 返回已支付 支付平台通过 Webhook 回调业务后端,前端再读取订单状态 支付平台通过 Webhook 回调业务后端,前端再读取订单状态 用户付款后,支付平台通过 Webhook 主动通知业务后端,前端再读取更新后的订单状态。
支付完成后的跳转页面只能用来改善用户体验。用户可能在支付后直接关掉页面,网络也可能在跳转时中断。订单最终状态应以后端收到并核实的支付结果为准。
Webhook 通常由后端接收。浏览器没有稳定的公网地址,用户关闭页面以后也无法继续接收请求。签名密钥同样不能放在前端代码里。
用最少的代码跑一次 下面用 Node.js 自带的 HTTP 模块写一个小例子,不需要安装任何框架。
新建 server.js。
const http = require(“node:http”);
const order = { id: “1001”, status: “pending”, };
const processedEvents = new Set();
const server = http.createServer((req, res) => { function reply(status, data) { res.writeHead(status, { “Content-Type”: “application/json; charset=utf-8”, }); res.end(JSON.stringify(data)); }
// 前端查询订单状态 if (req.method === “GET” && req.url === “/orders/1001”) { return reply(200, order); }
// 支付平台调用的 Webhook 地址 if (req.method === “POST” && req.url === “/webhooks/payment”) { let body = “”;
req.on("data", chunk => {
body += chunk;
});
req.on("end", () => {
let event;
try {
event = JSON.parse(body);
} catch {
return reply(400, { message: "invalid json" });
}
if (!event.id || !event.type) {
return reply(400, { message: "invalid event" });
}
// 同一个事件处理过以后,直接回复成功
if (processedEvents.has(event.id)) {
return reply(200, { received: true });
}
if (event.type === "payment.succeeded") {
if (event.data?.orderId !== order.id) {
return reply(404, { message: "order not found" });
}
order.status = "paid";
}
processedEvents.add(event.id);
return reply(200, { received: true });
});
return;
}
reply(404, { message: “not found” }); });
server.listen(3000, () => { console.log(“Server running at http://localhost:3000”); }); 运行它。
node server.js 先查询订单。
curl http://localhost:3000/orders/1001 得到的结果如下。
{“id”:“1001”,“status”:“pending”} 现在用 curl 假装自己是支付平台,向 Webhook 地址发送付款成功通知。
curl -X POST http://localhost:3000/webhooks/payment
-H “Content-Type: application/json”
-d ‘{
“id”: “evt_001”,
“type”: “payment.succeeded”,
“data”: {
“orderId”: “1001”
}
}’
服务器会回复。
{“received”:true} 再次查询订单。
curl http://localhost:3000/orders/1001 订单已经变成已支付。
{“id”:“1001”,“status”:“paid”} 这段代码里有三个字段需要留意。
id 是事件编号。支付平台重试通知时,可能再次发送同一个事件。后端用它判断自己有没有处理过。
type 表示发生了什么。payment.succeeded 代表付款成功,实际项目里还可能有付款失败、退款成功等类型。
data.orderId 指向业务系统里的订单。后端靠它找到需要修改的记录。
前端只需要查询自己的订单接口。
const order = await fetch(“/orders/1001”).then(response => response.json());
document.querySelector(“#status”).textContent = order.status === “paid” ? “支付成功” : “等待支付”; 学习阶段可以每隔几秒查一次订单。项目需要更及时的页面更新时,后端也可以通过 WebSocket 或 Server-Sent Events 通知前端。Webhook 接收第三方平台的消息,WebSocket 把结果推给浏览器,两者负责的方向不同。
Webhook 常见在哪些地方 支付回调是最常见的场景之一。支付平台确认收款后通知商家,退款和订阅扣款也可以沿用这种方式。
代码平台也经常使用 Webhook。有人向 GitHub 仓库推送代码以后,GitHub 可以通知构建服务器,随后开始测试或部署。GitHub 还支持 Pull Request、Release 和工作流运行等事件。
物流平台会在包裹揽收、运输状态变化或者签收后发送通知。电商后端收到消息,再更新用户看到的物流进度。
团队里使用多个在线工具时,Webhook 也很常见。表单收到新内容以后,可以通知客户管理系统。监控平台发现服务异常后,也能把消息发给告警机器人。
这些场景有一个共同点。事件发生在另一个系统里,而你的系统需要尽快知道结果。
写进生产项目以前还要补什么 上面的代码只用于看懂调用关系,没有实现签名验证,也没有连接数据库,不能原样放进正式项目。
收到请求以后先验签 任何人只要知道 Webhook 地址,都可能向它发送请求。
正规的第三方平台通常会给请求添加签名。你的后端使用平台提供的密钥重新计算签名,再与请求中的签名比较。匹配以后才继续处理业务。
GitHub 会把签名放进 X-Hub-Signature-256 请求头,并建议接收方在处理数据前完成验证。GitHub 签名验证说明
验签的过程可以画成这样。
不一致
一致
收到 Webhook 请求 取出请求头里的签名 用平台密钥重新计算签名 两处签名一致? 丢弃请求,返回 4xx 继续处理业务 实际接入支付平台时,优先使用平台官方 SDK 提供的验签方法。不同平台计算签名的规则可能不同,自己照着印象拼一套,很容易漏掉时间戳、原始请求体或者字符编码。
同一条通知可能收到多次 第三方平台发送 Webhook 后,如果没有及时收到成功响应,通常会再次发送。
这意味着“付款成功”通知可能来两遍。后端不能因此发两次优惠券、增加两次余额或者重复生成出库单。
常见做法是保存事件编号。已经成功处理过的编号再次出现时,直接返回成功,不再重复修改数据。这个要求通常叫幂等。
处理逻辑画出来就是这样。
处理过
没处理过
收到事件 事件编号处理过吗? 直接返回 2xx 处理业务 记录事件编号 返回 2xx 示例里的 processedEvents 只存在内存中,程序重启就会消失。正式项目应当把事件编号存进数据库,并让“记录事件”和“修改订单”尽量在同一个数据库事务中完成。
尽快返回成功状态 第三方平台关心你有没有收到通知。后端处理完成后,应当返回 200 或其他 2xx 状态码。
发邮件、生成报表之类的工作可能很慢。正式项目可以先保存事件,再返回成功,剩下的工作交给后台任务处理。
GitHub 建议 Webhook 接收端在十秒内响应,并推荐把较慢的处理放到后台执行。GitHub Webhook 使用建议
不要依赖通知顺序 网络请求可能延迟,第三方平台也可能重试。后产生的事件有机会先到,早一点的事件反而后到。
Stripe 明确说明,事件的送达顺序没有保证。业务需要先后关系时,可以读取事件中的时间和状态,必要时再调用第三方 API 查询最新结果。
日志要能查到一次完整调用 遇到回调问题时,至少要能查到事件编号、事件类型、订单编号、收到请求的时间和最终响应状态。
日志里不要直接记录密钥、完整签名、银行卡信息等敏感数据。
有了这些记录,你才能分清平台没有发送、请求没有到达、验签失败和业务处理报错。否则页面上都只会留下同一句“支付状态没有更新”。
配置 Webhook 时可以照着检查 接入一个新的 Webhook,通常要完成下面这些工作。
- 在自己的后端增加一个可以接收 POST 请求的地址。
- 到第三方平台填写这个地址,并选择需要订阅的事件。
- 配置密钥,在后端验证请求签名。
- 根据事件类型处理业务,并用事件编号避免重复执行。
- 返回 2xx,随后查看平台的发送记录和自己的服务器日志。 把这几步串起来看,就是下面这条线。
后端增加接收 POST 的地址 平台填写地址、订阅事件 配置密钥、后端验签 按事件类型处理业务 返回 2xx,查看发送记录与日志 本地开发时,localhost 只能被自己的电脑访问,第三方平台无法直接请求。可以使用平台提供的命令行工具,或者用临时公网隧道把请求转发到本机。Stripe CLI 就支持把测试事件转发到本地 Webhook 地址。
等这几个环节走通以后,Webhook 就没有那么神秘了。
你留下一个后端地址,第三方平台在事情发生后请求这个地址。后端核实消息、修改数据,再让前端读取新的结果。前端、后端和第三方平台之间的关系,也就接上了。
参考资料 • GitHub 对 Webhook 的介绍 • GitHub 创建和配置 Webhook • GitHub Webhook 安全与处理建议 • Stripe 接收 Webhook 事件