Appearance
Webhook 是什么?有哪些规范?如何用代码实现?(含 Golang 示例)

前言
你可能已经听说过 Webhook 这个词,也可能在 GitHub、Slack、钉钉或飞书的配置界面里见过它。它听起来像是一个“高级”的技术概念,但如果你从未实际用过,第一次接触时确实容易感到困惑——它到底是个什么东西?和普通的 API 调用有什么区别?为什么那么多平台都支持它?
这篇文章将从最基础的概念讲起,用通俗易懂的类比帮你理解 Webhook 的本质,然后深入探讨它目前有哪些“规范”(或者说是业界共识),最后用 Go 语言给出完整的代码示例,让你看完就能动手写代码。
一、Webhook 是什么?
1.1 一个生活化的类比
想象一下你去一家生意很火的餐厅吃饭。你点完餐后,有两种方式知道餐好了:
- 方式一(轮询):你每隔 2 分钟就去柜台问一次:“我的餐好了吗?”——这就是轮询(Polling)。你不停地主动去问,浪费自己的时间,也打扰服务员。
- 方式二(Webhook):你给服务员留下你的手机号,告诉 TA“餐好了打我电话”。然后你就可以安心坐着刷手机。等电话一响,你就知道可以去取餐了。——这就是 Webhook。
Webhook 本质上就是 一种由事件驱动的 HTTP 回调机制。用更技术一点的话说:应用 A 在某个事件发生时,自动向应用 B 预先指定的 URL 发送一个 HTTP 请求(通常是 POST),并在请求体中携带相关数据。
简单一句话:Webhook 就是“反向 API”——不是你主动去要数据,而是数据源主动把数据推给你。
1.2 Webhook vs. 传统 API
这是理解 Webhook 最关键的一步。
| 对比维度 | Webhook(事件驱动) | 传统 API(请求驱动) |
|---|---|---|
| 通信方向 | 推送(Push):数据源主动把数据发给你 | 拉取(Pull):你主动向数据源请求数据 |
| 触发方式 | 由事件触发(代码提交、支付完成、新用户注册等) | 由你的请求触发,需要定时或按需调用 |
| 实时性 | 近乎实时,事件发生即刻通知 | 取决于轮询频率,有延迟 |
| 资源消耗 | 低,只在有事件时才通信 | 高,需要频繁发送请求 |
| 典型场景 | GitHub 通知 CI/CD 流水线运行、支付回调通知订单系统 | 查询天气、获取用户列表、提交表单 |
搞懂了这个区别,你就已经理解了 Webhook 最核心的设计思想。
二、Webhook 是如何工作的?
一个完整的 Webhook 交互包含三个核心步骤,可以用下面的时序图清晰地展示:
三个步骤详解:
① 事件触发 在源应用中,一个预设的事件发生了。常见的例子包括:
- 有人在 GitHub 上 push 了代码
- Stripe 上收到了一笔付款
- 用户在新系统中完成了注册
② 发送 HTTP 请求 源应用根据你预先配置的 Webhook URL,构建一个 HTTP POST 请求,将事件相关的数据(Payload)放在请求体中发送出去。请求头中通常会包含用于验证的签名信息。
③ 接收与处理 你的应用在指定的 URL 上收到请求后,需要完成以下几件事:
- 验证请求签名,确保请求确实来自合法的发送方
- 解析请求体中的数据
- 立即将任务投递到消息队列,然后返回 200 OK(避免因耗时操作导致超时)
- 后台 Worker 异步消费任务,执行真正的业务逻辑(更新数据库、发送通知、触发后续流程等)
除了时序图,下面这张系统架构图可以展示 Webhook 在整个系统中的位置:
核心设计要点:
💡 为什么要在第三步立即返回 200 OK? Webhook 发送方通常有超时限制(如 GitHub 为 10 秒)和重试机制。如果你在 Webhook 处理函数中执行耗时操作(如复杂计算、多次数据库写入),一旦超过超时时间,发送方会认为请求失败并触发重试,导致重复处理。正确的做法是:验证签名后立即返回 200,将实际工作交给异步任务队列。
三、Webhook 有哪些规范?
这是很多初学者会问的问题,答案是:目前没有一个被广泛接受的强制性全球标准。
GitHub 的实现方式、Slack 的实现方式、钉钉的实现方式……每个平台在签名验证、重试机制、数据结构等细节上都有自己的做法。但这并不意味着没有“规范”可言——业界正在推动一些标准化草案,它们代表了未来的方向。
3.1 安全认证规范:SWT(Secure Webhook Token)
SWT 是 IETF(互联网工程任务组)提出的一个互联网草案,核心思路是使用一种专门用途的 JWT(JSON Web Token) 来验证 Webhook 请求的合法性。
它的关键设计包括:
- 传输:强制使用 HTTPS + HTTP POST。
- Token 位置:将 SWT 放在请求头的
Authorization: Bearer <token>字段,事件数据放在请求体。 - 标准声明(Claims):
webhook.event:事件类型,如payment.receivedwebhook.hash:请求体内容的加密哈希,保证数据完整性webhook.retry_count:当前是第几次重试exp、nbf、iat:过期时间、生效时间、签发时间,防重放攻击jti:JWT ID,防重放
遵循 SWT 可以让 Webhook 的认证达到企业级的安全水平。
3.2 流程控制规范:PCRP(Ping-Challenge-Resolve-Product)
这是另一个 IETF 草案,它关注的是 Webhook 交互的流程和生命周期管理,而不只是认证。
它通过一个自定义的 HTTP 头 X-PCRP-Type 来标识交互阶段:
ping:发送方检查接收方是否在线、是否准备好接收事件。challenge:接收方发起验证(如 CAPTCHA 或 Token 校验),确认发送方身份。resolve:发送方对 challenge 做出响应,提供验证凭证。product:这是最终的事件数据,只有在 ping 和 challenge 都成功后才会发送。
PCRP 还定义了 X-PCRP-Transaction-ID(事务ID)、X-PCRP-Nonce(防重放)等头部,让 Webhook 交互像 TCP 三次握手一样可靠。
注意:SWT 和 PCRP 目前都还处于“Internet-Draft”阶段,尚未成为正式标准。但理解它们可以帮你设计出更可靠、更安全的 Webhook 系统。
四、如何用 Golang 实现 Webhook?
接下来我们用 Go 语言来实际写代码,分两个场景:
- 发送 Webhook:在事件发生时,主动调用第三方平台的 Webhook URL(比如向 Slack、钉钉、飞书发通知)。
- 接收 Webhook:搭建一个 HTTP 服务,作为接收方来处理别人发过来的 Webhook 请求。
4.1 场景一:发送 Webhook 到 Slack
Slack、钉钉、飞书都支持“入站 Webhook(Incoming Webhook)”——其实就是它们给你一个 URL,你往这个 URL 发 POST 请求,就能把消息推送到频道或群里。
以下是用 Slack 官方 SDK 发送消息的示例:
go
package main
import (
"fmt"
"log"
"github.com/slack-go/slack"
)
func main() {
// 你在 Slack 创建 Incoming Webhook 时获得的 URL
webhookURL := "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX"
// 创建 Webhook 客户端
wc := slack.NewWebhook(webhookURL)
// 构造消息内容(支持 Slack Block Kit 丰富布局)
msg := &slack.WebhookMessage{
Text: "Hello! 这是一条来自 Go 应用的测试消息。",
Blocks: []slack.Block{
slack.NewSectionBlock(
slack.NewTextBlockObject("mrkdwn", "*新版本已部署* :rocket:", false, false),
nil,
nil,
),
},
}
// 发送
if err := wc.Send(msg); err != nil {
log.Fatalf("发送失败: %v", err)
}
fmt.Println("消息发送成功!")
}对于钉钉和飞书,原理完全一样——你拿到它们提供的 Webhook URL,按照它们要求的 JSON 格式发 POST 请求即可。区别只在 JSON 结构体和签名算法上。
4.2 场景二:接收 GitHub 的 Webhook
作为接收方,你需要做的就是启动一个 HTTP 服务器,监听一个 POST 路由。但最关键的一步是验证请求的合法性——否则任何人都可以向你的接口发假请求。
以下是处理 GitHub Webhook 的完整示例:
go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
)
// GitHub 事件 Payload 结构(根据需要补充更多字段)
type GitHubPayload struct {
Action string `json:"action"`
Repository struct {
FullName string `json:"full_name"`
} `json:"repository"`
Sender struct {
Login string `json:"login"`
} `json:"sender"`
}
// 验证 GitHub 签名
func verifySignature(body []byte, signatureHeader string, secret string) bool {
if signatureHeader == "" {
return false
}
// GitHub 的签名格式: sha256=xxxxx
expectedMAC := []byte(signatureHeader)
// 实际需要解析出 sha256= 后面的部分,此处简化示意
// 完整实现请参考: https://docs.github.com/zh/webhooks/securing
h := hmac.New(sha256.New, []byte(secret))
h.Write(body)
expected := hex.EncodeToString(h.Sum(nil))
// 注意:实际应使用恒定时间比较函数防止时序攻击
return hmac.Equal([]byte("sha256="+expected), []byte(signatureHeader))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
// 1. 只接受 POST
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 2. 【关键】验证签名
secret := os.Getenv("GITHUB_WEBHOOK_SECRET") // 从环境变量读取
signature := r.Header.Get("X-Hub-Signature-256")
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read body", http.StatusBadRequest)
return
}
defer r.Body.Close()
if !verifySignature(body, signature, secret) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// 3. 解析 JSON
var payload GitHubPayload
if err := json.Unmarshal(body, &payload); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// 4. 业务处理(此处建议异步处理,见下方最佳实践)
log.Printf("收到事件: %s,触发者: %s,仓库: %s",
payload.Action,
payload.Sender.Login,
payload.Repository.FullName,
)
// 5. 返回 200 确认
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "OK")
}
func main() {
http.HandleFunc("/webhook/github", webhookHandler)
log.Println("服务启动于 :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}五、最佳实践清单
无论你是用 Go 还是其他语言实现 Webhook,以下几点都是必须注意的:
| 实践项 | 说明 |
|---|---|
| 始终使用 HTTPS | 生产环境必须用 HTTPS,保证数据在传输过程中不被窃听或篡改。 |
| 严格验证签名 | 不要信任任何未经验证的请求。所有主流 Webhook 提供者都支持 HMAC 签名验证,这是你的第一道防线。 |
| 尽快返回 200 OK | 收到请求后,先验证签名,然后立即把任务丢进消息队列(如 Kafka、RabbitMQ)或异步任务队列,马上返回 200。不要在 Webhook 处理函数里做耗时操作,否则发送方可能超时并重试。 |
| 实现幂等性 | 网络问题可能导致发送方重试同一个 Webhook 请求。你的处理逻辑必须能识别并忽略重复请求,例如通过检查事件 ID(X-GitHub-Delivery 或 jti)来去重。 |
| 完善的日志与监控 | 记录所有收到的 Webhook 请求头、关键数据和错误信息。这在调试和问题排查时至关重要。 |
六、常见问题 FAQ
Q:Webhook 和回调(Callback)有什么区别?
A:两者本质上是同一件事——都是 HTTP 回调。但“Webhook”通常强调事件驱动和自动化,而“Callback”更常用于描述异步操作完成后通知结果的场景。在实际使用中,这两个词经常混用。
Q:如果我的服务挂了,Webhook 会丢失吗?
A:大多数主流平台(GitHub、Stripe、Slack 等)都有重试机制,如果接收方返回非 2xx 状态码或超时,它们会按一定的退避策略重试多次(通常是几天内)。但如果你希望万无一失,应该自己做好消息持久化和补偿机制。
Q:所有的 Webhook 都用 JSON 吗?
A:JSON 是目前最主流的选择,但不是强制要求。也有平台使用 XML、Form Data 甚至纯文本。不过绝大多数情况下,你都会和 JSON 打交道。
七、总结
| 问题 | 答案 |
|---|---|
| Webhook 是什么? | 一种事件驱动的 HTTP 回调,数据源主动把数据推送给你,无需你轮询。 |
| 和 API 有什么不同? | API 是你主动去拉取数据;Webhook 是别人主动把数据推给你。 |
| 有统一规范吗? | 目前无强制国际标准,但 IETF 有 SWT、PCRP 等草案;目前以各平台自己的实现为主。 |
| 如何用 Go 实现? | 发送方:构造 HTTP POST 请求到目标 URL;接收方:搭建 HTTP 服务,验证签名,异步处理业务。 |
Webhook 是一种极其强大的架构模式,它能让系统之间实现松耦合、事件驱动、近乎实时的通信。无论是构建 CI/CD 流水线、处理支付回调,还是对接各种 SaaS 服务,你都会频繁地和 Webhook 打交道。
希望这篇文章帮你彻底搞懂了 Webhook。
