本文详解 Python 中 HMAC 签名验证的正确实践:强调使用原始字节载荷、避免 JSON 序列化或字符串拼接导致的格式偏差,并推荐使用 hmac.compare_digest() 防御定时攻击。
本文详解 python 中 hmac 签名验证的正确实践:强调使用原始字节载荷、避免 json 序列化或字符串拼接导致的格式偏差,并推荐使用 `hmac.compare_digest()` 防御定时攻击。
在 Webhook 安全验证场景中,HMAC(Hash-based Message Authentication Code)是验证请求来源真实性与数据完整性的核心机制。许多开发者(如 Sumsub、Plaid 等平台)会在 HTTP 请求头中携带 X-Signature,并要求服务端用预共享密钥(secret key)对原始请求体(raw payload) 重新计算 HMAC,再比对结果。常见失败原因并非算法错误,而是载荷预处理方式不一致——例如手动拼接键值对、误用 json.dumps()、或对布尔值/空格/换行符等做非标准转换。
✅ 正确做法:直接使用原始字节流(raw bytes)计算 HMAC
Webhook 的真实 payload 是服务器发送的原始二进制数据(如 b'{\n "applicantId": "...", "sandboxMode": true\n}'),而非 Python 字典对象。任何中间转换(如 json.dumps(payload)、','.join(...) 或 .lower() 处理布尔值)都会引入格式差异,导致签名不匹配。
以下为生产就绪的验证函数示例:
import hmac
import hashlib
from flask import request # 示例基于 Flask;其他框架请替换为对应获取 raw body 的方式
def validate_webhook_signature(
secret_key: str,
signature_header: str = "X-Signature",
algorithm_header: str = "X-Payload-Digest-Alg",
allowed_algorithms: dict = None
) -> bool:
"""
验证 Webhook 请求的 HMAC 签名(防御定时攻击)
:param secret_key: 预共享密钥(字符串)
:param signature_header: 包含期望签名的请求头名
:param algorithm_header: 指定哈希算法的请求头名(如 'HMAC_SHA256_HEX')
:param allowed_algorithms: 算法映射字典,键为 header 值,值为 hashlib 函数
:return: 签名有效返回 True,否则抛出异常
"""
if allowed_algorithms is None:
allowed_algorithms = {
"HMAC_SHA1_HEX": hashlib.sha1,
"HMAC_SHA256_HEX": hashlib.sha256,
"HMAC_SHA512_HEX": hashlib.sha512,
}
# 1. 获取算法标识
algo_name = request.headers.get(algorithm_header)
if not algo_name or algo_name not in allowed_algorithms:
raise ValueError(f"Unsupported or missing digest algorithm: {algo_name}")
hash_func = allowed_algorithms[algo_name]
# 2. 获取原始请求体(关键!必须是 bytes)
# ✅ 正确:获取未解码的原始字节(保留换行、空格、大小写等所有细节)
raw_payload = request.get_data() # type: bytes
if not raw_payload:
raise ValueError("Empty payload received")
# 3. 计算 HMAC(使用 bytes 密钥 + raw_payload bytes)
key_bytes = secret_key.encode('utf-8')
computed_hmac = hmac.new(key_bytes, raw_payload, hash_func).hexdigest()
# 4. 获取请求头中的签名(通常为 hex 字符串)
expected_signature = request.headers.get(signature_header)
if not expected_signature:
raise ValueError(f"Missing required header: {signature_header}")
# 5. ✅ 安全比对:使用 hmac.compare_digest() 防御定时攻击
if not hmac.compare_digest(computed_hmac, expected_signature):
# 调试时可临时启用(上线前务必关闭)
# print(f"[DEBUG] Expected: {expected_signature!r}")
# print(f"[DEBUG] Computed: {computed_hmac!r}")
# print(f"[DEBUG] Payload (first 100 chars): {raw_payload[:100]!r}")
raise ValueError("HMAC signature validation failed")
return True
# 使用示例(Flask 视图中)
@app.route('/webhook', methods=['POST'])
def handle_webhook():
try:
validate_webhook_signature(secret_key="TZUQlLdW-E5VM7nbcByTbyQx9G_")
# ✅ 签名已通过验证,现在可安全解析 JSON
payload = request.get_json()
# ... 处理业务逻辑
return {"status": "ok"}, 200
except ValueError as e:
return {"error": str(e)}, 400
⚠️ 关键注意事项:
- 绝不手动构造 payload 字符串:json.dumps(payload) 默认无空格、sort_keys=False,而实际请求可能含缩进、换行或不同布尔字面量(true vs True)。在线工具显示的 "sandboxMode": true 是 JSON 格式规范写法,Python json.dumps() 输出 true(小写),但若原始请求含 True(首字母大写)或引号包裹("true"),则完全不兼容——因此唯一可靠输入是原始 request.get_data() 字节流。
- 编码一致性:secret_key.encode('utf-8') 和 request.get_data() 均为 bytes,无需额外 decode/encode。若误用 request.get_data(as_text=True) 再 encode,可能因系统默认编码或 BOM 引入不可见差异。
- 安全比对必须用 hmac.compare_digest():普通 == 比较在遇到前缀匹配时会提前退出,攻击者可通过响应时间差异推断签名字符,compare_digest() 保证恒定时间执行。
- 验证通过后再解析:仅在 HMAC 校验成功后,才调用 request.get_json() 解析 JSON,防止恶意篡改的无效 JSON 引发异常或 DoS。
总结:HMAC 验证的本质是「比特级一致性校验」。保持载荷字节原样、密钥字节原样、哈希算法一致,即可 100% 复现签名。任何试图“标准化” payload 文本格式的尝试,都是偏离协议的危险操作。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











