必须用request.get_data(cache=True, as_text=False)获取原始字节流,剥离X-Hub-Signature-256前缀后取哈希值,以hmac.compare_digest()恒定时间比对本地重算的SHA256签名,严禁使用request.json等破坏原始payload的方法。

Flask 接收 WebHook 时如何验证 X-Hub-Signature-256?
GitHub、GitLab、Stripe 等平台发来的 WebHook 请求,通常会在请求头里带一个 X-Hub-Signature-256,格式是 sha256=xxx。Flask 默认不自动解析原始 body,而签名校验必须用**未解码的原始字节流**和你自己的密钥重新计算哈希——用 request.get_json() 或 request.form 会破坏原始数据,导致校验失败。
实操要点:
- 必须用
request.get_data(cache=True, as_text=False)获取原始 body 字节(cache=True是关键,否则后续再调用会返回空) - 从
request.headers取X-Hub-Signature-256,用.split('=', 1)安全拆分,避免字段值含等号时出错 - 用
hmac.compare_digest()校验(不是==),防止时序攻击 - 密钥必须是 bytes 类型,用
b'your-secret'或SECRET.encode()
import hmac
import hashlib
<p>@app.route('/webhook', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Hub-Signature-256')
if not signature or not signature.startswith('sha256='):
return 'Invalid signature', 400</p><pre class="brush:php;toolbar:false;">expected_hash = signature.split('=', 1)[1]
body = request.get_data(cache=True, as_text=False)
secret = b'your-webhook-secret'
computed_hash = hmac.new(secret, body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(computed_hash, expected_hash):
return 'Signature mismatch', 400
# 校验通过,继续处理
data = request.get_json()
return 'OK', 200
为什么 request.json 和 request.get_json() 不能用于签名校验?
这两个方法会先尝试解析 JSON,内部调用 request.get_data(as_text=True),这会导致原始 body 被读取并解码为字符串(比如把 \n 视为换行、把 Unicode 转义还原),而签名是基于服务器收到的原始字节算的。哪怕只是多一个空格、少一个反斜杠,哈希就完全不一样。
立即学习“Python免费学习笔记(深入)”;
常见错误现象:
- 本地测试用 curl 发 raw JSON 能过,但 GitHub 实际回调总失败
- 日志里看到
computed_hash和expected_hash明显不同,但肉眼看不出差异 - 把 body 打印出来是正常 JSON,却校验失败——那说明它已经被 Flask “美化”过了
解决办法只有一条:校验前绝不碰 request.json、request.form、request.args,所有解析动作放在 hmac.compare_digest() 之后。
其他常见 WebHook 签名头怎么处理?
不同平台用的头名和算法不同,但流程一致:取头、取原始 body、用密钥重算、安全比对。注意它们的格式细节:
- GitLab:
X-Gitlab-Token是明文 token(不是哈希),直接比对字符串即可;但也有可选的X-Gitlab-Event+ body 签名模式,需配shared_secret - Stripe:
Stripe-Signature是复合签名,含时间戳和多个哈希,要用stripe.Webhook.construct_event()解析,不能手写 - Slack:
X-Slack-Signature和X-Slack-Request-Timestamp联合校验,且要求 timestamp 与服务器时间差 - 微信公众号:
X-WX-Nonce+X-WX-Timestamp+X-WX-Signature,需按文档拼接字符串再 SHA1
别硬套 GitHub 模式——先查清平台文档里写的「签名生成方式」和「头字段名」,再决定用 HMAC 还是拼接校验。
Flask 中容易被忽略的配置陷阱
即使逻辑写对,也可能因 Flask 配置或部署环境出问题:
- 用 Gunicorn/Nginx 时,如果 Nginx 配了
proxy_buffering on或client_max_body_size太小,可能截断 body,导致校验失败 - Flask 的
JSON_SORT_KEYS=True(默认)不影响签名校验,但如果你在签名校验后手动json.dumps(data)再比对,排序会导致哈希不一致 - 调试时用
print(request.get_data())一次就清空 buffer,后面再get_data()就拿不到数据——务必加cache=True - 本地开发用 Flask 自带 server 没问题,但上线后若用了 ASGI 服务器(如 Uvicorn),要确认中间件没提前消费 body
最稳妥的做法:签名校验逻辑封装成装饰器,开头就固定调用 request.get_data(cache=True),后面所有业务逻辑都基于这个前提——别指望“后面再读一次”。


















