hmac.new()的key和msg参数必须为bytes类型,传入str会报TypeError;需对字符串显式调用encode("utf-8"),且验证时须用hmac.compare_digest()防时序攻击。

hmac.new() 的 key 和 msg 参数必须是 bytes 类型
Python 的 hmac.new() 不接受 str,传入字符串会直接报 TypeError: key: expected bytes, but got str。这是最常见的卡点,尤其在从配置读 secret 或拼接消息时容易忽略编码。
正确做法是显式 encode:
import hmac
import hashlib
<p>secret_key = b"my_secret" # 必须是 bytes
msg = "hello|2024-06-15T12:00:00".encode("utf-8") # 也必须是 bytes</p><p>sig = hmac.new(secret_key, msg, hashlib.sha256).digest()如果 secret 来自环境变量或配置文件(比如 os.getenv("API_SECRET")),务必加 .encode("utf-8");否则即使内容一致,签名也会不匹配。
立即学习“Python免费学习笔记(深入)”;
服务端验证时必须用 constant_time_compare 防时序攻击
直接用 == 比较两个签名,可能被攻击者通过响应时间差异推测出正确签名的字节——尤其当签名用于 API 认证时风险真实存在。
推荐用 hmac.compare_digest()(Python 2.7+/3.3+ 内置):
received_sig = bytes.fromhex("a1b2c3...") # 客户端传来的 hex 签名
expected_sig = hmac.new(secret_key, msg, hashlib.sha256).digest()
<p>if not hmac.compare_digest(received_sig, expected_sig):
raise ValueError("Signature mismatch")- 不要用
received_sig == expected_sig - 不要自己写循环逐字节比较
- 如果收到的是 base64 或 hex 编码的签名,先用
bytes.fromhex()或base64.b64decode()还原为原始 bytes 再比对
加盐(salt)不是单独传参,而是混入 key 或 msg
hmac 本身不提供 salt 参数。所谓“加盐”,实际是把 salt 作为 key 的一部分,或拼进 msg——但二者语义不同:
- 把 salt 当作 key 的前缀/后缀(如
b"salt_" + secret_key):salt 参与密钥派生,影响整个 HMAC 密钥空间 - 把 salt 拼进 msg(如
msg + b"|salt=abc123"):salt 成为消息一部分,验证时必须严格复现相同拼接顺序和分隔符
更安全的做法是 salt 与 secret 组合为最终 key:
salt = b"per-request-nonce-123" final_key = hmac.new(salt, secret_key, hashlib.sha256).digest() sig = hmac.new(final_key, msg, hashlib.sha256).digest()
这样既复用 salt 的随机性,又避免暴露原始 secret。
HTTP Header 中传递签名时注意编码和分隔格式
常见错误是把 raw bytes 直接塞进 header,导致乱码或截断。必须编码:
- 推荐 hex:用
sig.hex()发送,服务端用bytes.fromhex() - 也可 base64:用
base64.b64encode(sig).decode("ascii"),服务端用base64.b64decode()
Header 建议带明确字段名和算法标识,例如:
Authorization: HMAC-SHA256 signature=a1b2c3..., payload=hello|2024-06-15T12:00:00
注意:payload 字段值必须和服务端拼接的完全一致(包括空格、换行、编码),否则签名必然失败——这是调试时最常漏查的一环。
签名逻辑看似简单,但 key 类型、比较方式、salt 融合位置、传输编码这四点任一出错都会导致验签静默失败。尤其跨语言对接时,Python 的 bytes 意识和 compare_digest 使用习惯往往和对方不一致。


















