Python Webhook 接收端的签名验证

1次阅读

必须先用 request.get_data(cache=true) 一次性读取原始 body 字节,再基于它验签;签名头名依平台而异(如 github 用 x-hub-signature-256),密钥须为 bytes 类型,且必须用 hmac.compare_digest 防时序攻击。

Python Webhook 接收端的签名验证

怎么验证 python Webhook 请求的签名

签名验证不是加个 hmac.compare_digest 就完事——它依赖请求体原始字节、签名头字段名、密钥编码方式,三者错一个就永远验不过。

常见错误现象:Signature mismatchInvalid signature,但用 postman 手动构造却能过;本质是 flask/fastapi 默认解析了 request.body,导致读取后 body 流已耗尽,再读就是空字节。

  • 必须在解析 json 或 form-data 前,先用 request.get_data() 一次性读取原始 body(设 cache=True 防重复读)
  • 签名头名因平台而异:GitHub 用 X-Hub-Signature-256,Stripe 用 Stripe-Signature,Slack 用 X-Slack-Signature,不能硬写死
  • 密钥必须是 bytes 类型,bytes(secret, "utf-8")secret.encode() 更显式,避免 str/bytes 混用出 silent bug

Flask 里怎么安全读取原始 body 并验签

Flask 的 request.data 在未触发 request.jsonrequest.form 时可用,但一旦触发,request.data 就变空——这不是 bug,是 Werkzeug 的流设计。

使用场景:接收 GitHub push 事件、Stripe 支付回调等需严格验签的 webhook。

立即学习Python免费学习笔记(深入)”;

  • 在视图函数开头立刻调用 raw_body = request.get_data(cache=True),后续所有解析都基于这个变量
  • 验签时用 hmac.new(key, raw_body, digestmod="sha256").hexdigest(),再与 header 中值比对(注意 GitHub 的 header 值带 sha256= 前缀,要切掉)
  • 别用 request.json 直接解析,改用 json.loads(raw_body),否则 body 被二次解析可能失败(如含非 UTF-8 字节)

FastAPI 怎么绕过自动解析做验签

FastAPI 默认把 body 当 JSON 解析并注入参数,不给你碰原始字节的机会——除非显式声明依赖 Request 对象

性能影响:每次请求多一次内存拷贝(await request.body()),但对 webhook 这种低频高敏场景可接受。

  • 函数签名必须包含 request: Request,不能只靠 Pydantic 模型自动绑定
  • raw_body = await request.body() 获取 bytes,不要用 await request.json()——后者内部已 decode,无法还原原始字节
  • 若同时需要解析 JSON 和验签,先 await request.body(),再 json.loads(raw_body),最后用同一份 raw_body 计算签名

为什么用 hmac.compare_digest 而不用 ==

这是防时序攻击的硬性要求。普通字符串比较在遇到第一个不匹配字节时就返回,攻击者可通过响应时间差反推签名内容。

兼容性注意:Python hmac.compare_digest,但现代项目基本不需兼容这么老的版本。

  • 必须传入两个 bytes 类型参数,传 str 会报 TypeError
  • header 中的签名通常是 hex 字符串(如 sha256=abc123...),需先提取 hash 部分再转 bytes,别直接和 hex 字符串比
  • 哪怕只是本地开发调试,也别图省事换成 ==,否则上线后补救成本远高于初期多写一行

验签逻辑本身不复杂,真正容易被忽略的是「body 必须且只能读一次」这个前提——它藏在框架文档角落,却决定整个验证是否可信。

text=ZqhQzanResources