Webhook签名验证需严格遵循三要点:正确解析签名头(剥离前缀或提取字段)、仅一次读取原始payload并复用、使用hmac.Equal恒定时间比对;必须校验时间戳防重放,且密钥与字节一致性须实测验证。

Webhook 签名验证不是“加个 HMAC-SHA256 就完事”,关键在于签名生成逻辑、时间戳容差、编码一致性这三点对不上,401 Unauthorized 或 signature mismatch 就会反复出现。
如何正确提取并解析签名头(如 X-Hub-Signature-256)
GitHub、Stripe、Slack 等平台签名头命名不统一,且值格式有前缀(如 sha256=...),直接用 req.Header.Get("X-Hub-Signature-256") 拿到的是带前缀字符串,不剥离就无法比对。
- 必须用
strings.TrimPrefix剥离前缀,例如:sig := strings.TrimPrefix(req.Header.Get("X-Hub-Signature-256"), "sha256=") - 部分服务(如 Stripe)用
Stripe-Signature,其值是逗号分隔的多个字段(t=171... , v1=..., v0=...),需先按,拆分再提取v1字段 - 空值或缺失头要提前返回 400,避免后续 panic;建议用
if sig == "" { http.Error(w, "missing signature", http.StatusBadRequest); return }
原始 payload 必须用 io.ReadAll 一次性读取,不能重复读 req.Body
req.Body 是单次读取流,若你在日志里先 io.ReadAll(req.Body) 打印调试,后面再读一次——第二次读到的就是空字节,签名必然失败。
- 正确做法:只读一次,把结果存为
payload变量,后续签名计算和业务逻辑都复用它 - 如果需要保留
req.Body给下游 handler(比如用http.StripPrefix后转给 Gin),得用io.NopCloser(bytes.NewReader(payload))重建一个可重读的 Body - 别用
req.FormValue或req.PostForm解析 payload —— Webhook 多数是application/json或application/x-www-form-urlencoded,但签名是对原始字节算的,不是对解析后的 map
签名比对必须用 hmac.Equal,严禁用 == 或 bytes.Compare
直接用 == 比较会导致时序攻击风险,Go 标准库明确要求用 hmac.Equal 做恒定时间比对。而且 hmac.Sum().Sum(nil) 返回的是带哈希前缀的字节,而签名头是 hex 编码字符串,必须统一转成 []byte 再比。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 计算本地签名:用
hmac.New+sha256.New,写入payload,再用hex.EncodeToString(sum.Sum(nil))得到字符串 - 将 header 中提取的签名字符串用
hex.DecodeString转成[]byte - 最后用
hmac.Equal(localSigBytes, headerSigBytes)判断,返回 bool - 注意:有些平台(如 Discord)签名是 base64 编码,不是 hex,解码要用
base64.StdEncoding.DecodeString
时间戳验证不是可选项,而是防重放攻击的核心防线
多数 Webhook(GitHub、Stripe)在签名 payload 外额外附带时间戳(如 X-Hub-Signature-256 同级的 X-Hub-Timestamp),不校验它等于裸奔。
- 从 header 读出时间戳后,用
strconv.ParseInt转成int64,再用time.Unix(timestamp, 0)构造时间 - 与
time.Now().UTC()比较,容差建议设为 5 分钟(5 * time.Minute),太长易被重放,太短可能因服务器时钟不同步误杀 - 如果时间戳早于当前时间减去容差,或晚于当前时间加容差,直接拒绝 —— 不要只 warn,要
http.Error(w, "...", http.StatusForbidden) - 别忘了:时间戳本身是签名的一部分(如 GitHub 是
timestamp + payload拼接后签名),所以校验顺序必须是「先验时间戳有效性 → 再验签名」
最常被跳过的细节是:签名密钥是否被意外硬编码、环境变量加载失败导致用空字符串当 key,以及 payload 是否包含不可见字符(如 BOM)。上线前务必用真实平台发一条测试事件,抓包对比两端原始字节是否完全一致。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










