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

怎么验证 python Webhook 请求的签名
签名验证不是加个 hmac.compare_digest 就完事——它依赖请求体原始字节、签名头字段名、密钥编码方式,三者错一个就永远验不过。
常见错误现象:Signature mismatch、Invalid 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.json 或 request.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 必须且只能读一次」这个前提——它藏在框架文档角落,却决定整个验证是否可信。