
本文详解如何正确验证 webhook 的 hmac 签名,强调必须使用原始字节流(而非 json 字符串或手动拼接)计算摘要,并推荐使用 hmac.compare_digest() 防御定时攻击。
本文详解如何正确验证 webhook 的 hmac 签名,强调必须使用原始字节流(而非 json 字符串或手动拼接)计算摘要,并推荐使用 hmac.compare_digest() 防御定时攻击。
在 Webhook 安全验证场景中,HMAC(Hash-based Message Authentication Code)是保障数据完整性与来源可信的核心机制。常见错误在于:对 payload 进行非原始格式的预处理(如手动字符串拼接、json.dumps() 格式化、布尔值转小写等),导致计算出的签名与服务端不一致。关键原则是:验证阶段应将请求体视为不可变的二进制 blob,直接对其原始字节计算 HMAC。
✅ 正确做法:使用原始请求字节
假设你通过 Flask 或其他框架接收 Webhook 请求,服务端发送的签名基于原始 JSON 字节流(含换行、空格、原始 true/false 等),那么你的验证逻辑必须严格复现这一过程:
import hmac
import hashlib
from flask import request
def validate_webhook_signature(secret_key: str, expected_signature: str,
algo_name: str = 'HMAC_SHA256_HEX') -> bool:
# 1. 映射算法名称到哈希函数
algo_map = {
'HMAC_SHA1_HEX': hashlib.sha1,
'HMAC_SHA256_HEX': hashlib.sha256,
'HMAC_SHA512_HEX': hashlib.sha512,
}
hash_func = algo_map.get(algo_name)
if not hash_func:
raise ValueError(f"Unsupported algorithm: {algo_name}")
# 2. 获取原始请求体字节(关键!禁止 decode 再 encode)
raw_payload = request.get_data() # type: bytes
if not raw_payload:
raise ValueError("Empty payload received")
# 3. 计算 HMAC(使用字节密钥和原始字节 payload)
key_bytes = secret_key.encode('utf-8')
computed_hmac = hmac.new(key_bytes, raw_payload, hash_func).hexdigest()
# 4. 使用恒定时间比较(防御定时攻击)
if not hmac.compare_digest(computed_hmac, expected_signature):
# 可选:记录调试信息(生产环境应禁用)
# print(f"DEBUG - Raw payload: {raw_payload[:200]!r}")
# print(f"DEBUG - Expected: {expected_signature!r}")
# print(f"DEBUG - Computed: {computed_hmac!r}")
return False
return True
# 示例调用(Flask 路由中)
@app.route('/webhook', methods=['POST'])
def handle_webhook():
secret = 'TZUQlLdW-E5VM7nbcByTbyQx9G_'
expected_sig = request.headers.get('X-Signature')
algo = request.headers.get('X-Payload-Digest-Alg', 'HMAC_SHA256_HEX')
if not validate_webhook_signature(secret, expected_sig, algo):
return {'error': 'Invalid signature'}, 401
# ✅ 签名验证通过后,才解析 JSON
try:
data = request.get_json()
# 处理业务逻辑...
return {'status': 'ok'}, 200
except Exception as e:
return {'error': 'Invalid JSON'}, 400
⚠️ 常见陷阱与注意事项
❌ 绝对不要手动拼接或格式化 payload
如 ','.join(...) 或 json.dumps(payload, sort_keys=True) —— 即使空格、换行、引号、布尔值大小写(true vs "true")的微小差异都会导致签名不匹配。❌ 避免先解码再编码
request.get_data(as_text=True).encode('utf-8') 在特殊字符(如 BOM、代理对)下可能引入不可见差异;get_data() 直接返回原始字节才是唯一可靠方式。✅ 密钥编码统一为 UTF-8
secret_key.encode('utf-8') 是标准做法,确保与服务端一致(注意:某些服务端可能要求 Base64 解码密钥,需查阅文档)。✅ 使用 hmac.compare_digest()
普通 == 比较存在定时攻击风险(攻击者可通过响应时间推测签名字符),compare_digest() 提供恒定时间比较,是安全实践的强制要求。? 调试建议(临时启用)
若验证失败,可打印 raw_payload[:100] 和 repr(raw_payload[:100]) 对比服务端实际发送内容,确认是否含 BOM、UTF-8-BOM、CRLF 等隐藏字符。
总结
HMAC 验证的本质是「字节级一致性校验」。成功的前提是:客户端与服务端对同一段原始字节应用完全相同的密钥和哈希算法。任何中间格式转换(JSON 序列化、字符串标准化、类型转换)都是破坏一致性的根源。始终以 request.get_data() 获取原始字节,并在签名验证通过后才进行 JSON 解析与业务处理——这是兼顾安全性、鲁棒性与兼容性的最佳实践。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











