Python HMAC hmac.new() SHA-256
直接在浏览器中使用免费的 HMAC Generator,无需安装。
在线试用 HMAC Generator →每个 Webhook 回调、每个签名 API 请求、每条 Stripe 或 GitHub 事件通知,都使用 HMAC 签名来证明载荷未被篡改。Python 的 hmac 模块通过一次函数调用即可处理 Python HMAC-SHA256: hmac.new(key, msg, hashlib.sha256)。 无需 pip 安装,无需 C 扩展,无需第三方依赖。如需快速进行一次性签名校验而无需编写代码, 在线 HMAC 生成器 可立即给出结果。 本文涵盖 hmac.new()、 hmac.digest()、 hmac.compare_digest()、 Base64 编码、Webhook 验证、API 请求签名,以及从 SHA-1 到 SHA-512 的所有哈希算法。所有示例均以 Python 3.7+ 为目标。
- ✓hmac.new(key, msg, hashlib.sha256) 是标准入口——key 和 msg 必须是字节,digestmod 自 Python 3.4 起为必填参数。
- ✓hmac.digest(key, msg, "sha256") 是 Python 3.7 新增的更快一次性替代方案——返回原始字节,无中间对象。
- ✓始终使用 hmac.compare_digest() 验证签名以防止时序攻击——绝不用 == 进行 HMAC 比较。
- ✓将原始 .digest() 输出进行 Base64 编码用于 HTTP 头部和 Webhook 签名:base64.b64encode(h.digest())。
- ✓hmac 模块接受任何 hashlib 算法:sha1、sha256、sha384、sha512、md5、blake2b。
什么是 HMAC?
HMAC(基于哈希的消息认证码)是定义于 RFC 2104 的一种构造,它将密钥与哈希函数结合,生成固定大小的认证标签。与普通哈希(任何人均可计算)不同,HMAC 需要知道密钥。这意味着你可以用它同时验证消息的完整性和真实性。哪怕消息或密钥中只改变一个字节,输出也会完全不同。该构造通过将密钥与两个不同的填充常量(ipad 和 opad)异或,在两次哈希运算之间包裹消息来实现。Python 的 hmac 模块直接实现了这一 RFC 规范。
# 普通 SHA-256 哈希——无密钥,任何人均可计算 hashlib.sha256(b"payment:9950:USD").hexdigest() # "7a3b1c..." (确定性,公开)
# HMAC-SHA256——需要密钥才能生成 hmac.new(b"api_secret", b"payment:9950:USD", hashlib.sha256).hexdigest() # "e4f2a8..." (仅密钥持有者可计算)
hmac.new() — 标准库入口
hmac 模块是 Python 标准库的一部分。两行导入即可就绪: import hmac, hashlib。 模块的三个主要函数是 hmac.new() (创建 HMAC 对象)、 hmac.digest() (一次性,Python 3.7+)和 hmac.compare_digest() (恒定时间比较)。无需 pip 安装。
hmac.new(key, msg, digestmod) 接受三个参数。 key 和 msg 都必须是字节类对象( bytes、 bytearray 或 memoryview)。digestmod 参数自 Python 3.4 起为必填,接受任意 hashlib 构造函数(如 hashlib.sha256) 或字符串名称如 "sha256"。
import hmac
import hashlib
key = b"webhook_signing_key_2026"
message = b'{"event":"invoice.paid","invoice_id":"inv_8f3a","amount":19900}'
# 创建 HMAC 对象并获取十六进制签名
signature = hmac.new(key, message, hashlib.sha256).hexdigest()
print(signature)
# "b4e74f6c9a1d3e5f8b2a7c0d4e6f1a3b5c7d9e0f2a4b6c8d0e1f3a5b7c9d0e2f"HMAC 对象提供两种输出方法。 .digest() 返回原始字节(SHA-256 为 32 字节,SHA-512 为 64 字节)。 .hexdigest() 返回小写十六进制字符串。十六进制字符串是普通的 Python str——无需解码步骤。
import hmac import hashlib key = b"service_auth_key" msg = b"GET /api/v2/orders 2026-03-28T14:30:00Z" h = hmac.new(key, msg, hashlib.sha256) raw_bytes = h.digest() print(type(raw_bytes), len(raw_bytes)) # <class 'bytes'> 32 hex_string = h.hexdigest() print(type(hex_string), len(hex_string)) # <class 'str'> 64 # 两者表示相同的数据——十六进制只是字节的字符串编码 assert raw_bytes.hex() == hex_string
如果密钥或消息是 Python 字符串,在传入 hmac.new() 之前需调用 .encode() 将其转换为字节。几乎每个人第一次都会在这里踩坑——Python 3 字符串是 Unicode,而非字节,hmac 模块会拒绝它们。
import hmac
import hashlib
# 字符串密钥和消息——.encode() 转换为 UTF-8 字节
api_key = "sk_live_9f3a2b7c4d8e"
request_body = '{"customer_id":"cust_4421","plan":"enterprise"}'
signature = hmac.new(
api_key.encode(),
request_body.encode(),
hashlib.sha256
).hexdigest()
print(signature)
# "3a9f1b..." — 一致的十六进制字符串输出digestmod 参数自 Python 3.4 起无默认值。 不传此参数调用 hmac.new(key, msg) 会抛出 TypeError。3.4 之前默认为 MD5, 正因如此 Python 维护者删除了默认值——迫使开发者做出明确、安全的选择。HMAC-SHA256 Base64、SHA-1、SHA-512 与 MD5
hmac.new() 函数可与 hashlib 中任何可用的哈希算法配合使用。大多数 Webhook 提供商和 API 网关使用 HMAC-SHA256,但你也会在 OAuth 1.0a 中遇到 SHA-1,在强制要求的协议中遇到 SHA-512,在尚未迁移的旧系统中遇到 MD5。
HMAC-SHA256 与 Base64 输出
许多 Webhook 提供商在 HTTP 头部中以 Base64 编码字符串发送签名。要生成相同格式,将原始 .digest() 字节传入 base64.b64encode() 即可。
import hmac
import hashlib
import base64
key = b"whsec_MbkP7x9yFqHGn3tRdWz5"
payload = b'{"id":"evt_1Nq","type":"charge.succeeded","data":{"amount":4200}}'
# 原始摘要 → Base64(常见于 Authorization 头部和 Webhook 签名)
raw_digest = hmac.new(key, payload, hashlib.sha256).digest()
b64_signature = base64.b64encode(raw_digest).decode("ascii")
print(b64_signature)
# "dGhpcyBpcyBhIHNhbXBsZSBzaWduYXR1cmU="
# 这是与 X-Signature 头部比较的值
header_value = f"sha256={b64_signature}"
print(header_value)
# "sha256=dGhpcyBpcyBhIHNhbXBsZSBzaWduYXR1cmU="HMAC-SHA1 — 旧协议兼容
SHA-1 在新设计中被认为较弱,但 HMAC-SHA1 仍被 OAuth 1.0a 和一些旧版 Webhook 实现所要求。代码完全相同——只需换用算法即可。
import hmac
import hashlib
consumer_secret = b"oauth_consumer_secret_2026"
token_secret = b"oauth_token_secret_2026"
# OAuth 1.0a 使用 consumer_secret&token_secret 作为签名密钥
signing_key = consumer_secret + b"&" + token_secret
base_string = b"GET&https%3A%2F%2Fapi.service.com%2Fv1%2Forders&oauth_nonce%3D7f3a91bc"
sig = hmac.new(signing_key, base_string, hashlib.sha1).digest()
import base64
oauth_signature = base64.b64encode(sig).decode("ascii")
print(oauth_signature)
# "Tza3R9sE..." — 对此进行 URL 编码用于 Authorization 头部HMAC-SHA512 — 更长输出
import hmac
import hashlib
key = b"high_security_signing_key_64_bytes_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
msg = b'{"transfer_id":"xfr_9c2e","amount":500000,"currency":"EUR"}'
h = hmac.new(key, msg, hashlib.sha512)
print(len(h.digest())) # 64 字节(512 位)
print(len(h.hexdigest())) # 128 个十六进制字符
print(h.hexdigest()[:40] + "...")
# "8e3a1f9b2c4d6e7f0a1b3c5d7e9f0a2b4c6d8e0f..."HMAC-MD5 — 仅用于旧系统
import hmac import hashlib # MD5 在密码学上已被破解——仅用于旧协议兼容 key = b"legacy_api_key" msg = b"action=charge&amount=1500&merchant=store_42" sig = hmac.new(key, msg, hashlib.md5).hexdigest() print(sig) # 32 字符十六进制字符串 # "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"
hmac.new() 参数参考
构造函数签名为 hmac.new(key, msg=None, digestmod)。 模块中的三个主要函数对密钥和算法参数使用相同模式。
hmac.new() 构造函数
hmac.digest() 一次性函数(Python 3.7+)
digestmod 参数接受可调用对象(如 hashlib.sha256) 或字符串名称(如 "sha256")。 推荐使用可调用形式,因为它在导入时即被验证——字符串形式中的拼写错误只在运行时才会报错。
hmac.digest() — 快速一次性 HMAC(Python 3.7+)
Python 3.7 新增了 hmac.digest(key, msg, digest) 作为模块级函数。它在一次调用中完成 HMAC 计算,无需创建中间 HMAC 对象。返回值是原始字节(等同于对对象调用 .digest())。 该函数在 CPython 上使用优化的 C 实现,避免了对象分配开销,在频繁调用的场景中速度明显更快。
import hmac
import hashlib
key = b"batch_signing_key_2026"
messages = [
b'{"order_id":"ord_001","total":4500}',
b'{"order_id":"ord_002","total":8900}',
b'{"order_id":"ord_003","total":2200}',
]
# 一次性摘要——无中间 HMAC 对象
signatures = [hmac.digest(key, msg, hashlib.sha256) for msg in messages]
# 转换为十六进制便于展示
for msg, sig in zip(messages, signatures):
print(f"{msg[:30]}... -> {sig.hex()[:24]}...")限制: hmac.digest() 只返回原始字节。如果直接需要十六进制字符串,仍然需要 hmac.new() 的 .hexdigest() 方法,或在字节结果上链式调用 .hex()。
hmac.digest() 不支持增量 .update() 调用。如果需要分块读取大文件并对内容计算 HMAC, 请使用 hmac.new() 并在循环中调用 .update(chunk)。验证来自 Webhook 和 API 响应的 HMAC 签名
Python 中 HMAC 最常见的用途是验证 Webhook 签名。每个主流提供商(Stripe、GitHub、Shopify、Twilio)都使用 HMAC-SHA256 对载荷签名,并在头部中发送签名。模式始终相同:对原始请求体重新计算 HMAC,然后使用 hmac.compare_digest() 进行比较。
Webhook 签名验证
import hmac
import hashlib
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = b"whsec_MbkP7x9yFqHGn3tRdWz5"
@app.route("/webhooks/payments", methods=["POST"])
def handle_payment_webhook():
# 获取原始请求体——必须与签名时的内容完全一致
raw_body = request.get_data()
# 从头部获取签名
received_sig = request.headers.get("X-Signature-256", "")
# 对原始请求体重新计算 HMAC
expected_sig = hmac.new(WEBHOOK_SECRET, raw_body, hashlib.sha256).hexdigest()
# 恒定时间比较——防止时序攻击
if not hmac.compare_digest(f"sha256={expected_sig}", received_sig):
abort(403, "Invalid signature")
# 签名验证通过——处理事件
event = request.get_json()
print(f"Verified event: {event['type']} for {event['data']['amount']}")
return "", 200hmac.compare_digest() 函数以恒定时间比较两个字符串或字节序列。普通的 == 比较在第一个不匹配字节处短路。攻击者可以通过多次请求测量响应时间,逐字节重建预期签名。恒定时间比较消除了这一旁信道。
GitHub Webhook 验证
GitHub 的 Webhook 格式展示了完整的模式。它发送一个 X-Hub-Signature-256 头部,内容为 sha256= 后跟原始请求体的十六进制 HMAC-SHA256,使用你在仓库设置中配置的 Webhook 密钥签名。与通用 Webhook 验证的关键区别在于:比较前必须去掉 sha256= 前缀,且必须读取请求体的原始字节——先解析 JSON 会改变字节表示,导致验证失败。
import hmac
import hashlib
from flask import Flask, request, abort
app = Flask(__name__)
GITHUB_WEBHOOK_SECRET = b"your_github_webhook_secret"
@app.route("/webhooks/github", methods=["POST"])
def handle_github_webhook():
# GitHub 发送:X-Hub-Signature-256: sha256=<hex_digest>
sig_header = request.headers.get("X-Hub-Signature-256", "")
if not sig_header.startswith("sha256="):
abort(403, "Missing or malformed signature header")
received_hex = sig_header[len("sha256="):]
raw_body = request.get_data() # 原始字节——在此之前不要解析 JSON
expected_hex = hmac.new(
GITHUB_WEBHOOK_SECRET, raw_body, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected_hex, received_hex):
abort(403, "Signature mismatch — payload may have been tampered with")
event_type = request.headers.get("X-GitHub-Event", "unknown")
payload = request.get_json()
print(f"Verified GitHub {event_type} event: {payload.get('action', '')}")
return "", 200同样的模式适用于 Shopify(X-Shopify-Hmac-SHA256) 和 Twilio(X-Twilio-Signature), 唯一区别是头部名称以及签名是十六进制还是 Base64 编码。务必查阅提供商文档确认编码格式——混淆十六进制和 Base64 是签名不匹配错误最常见的原因。
使用 HMAC 进行 API 请求认证
某些 API 要求客户端对每个请求进行 HMAC 签名。签名字符串通常包含 HTTP 方法、路径、时间戳和请求体。以下是用于服务间内部认证的模式。
import hmac
import hashlib
import time
import json
def sign_request(secret: bytes, method: str, path: str, body: str) -> dict:
"""为 API 请求生成 HMAC-SHA256 签名。"""
timestamp = str(int(time.time()))
# 构建签名字符串——方法 + 路径 + 时间戳 + 请求体
signing_string = f"{method}\n{path}\n{timestamp}\n{body}"
signature = hmac.new(
secret,
signing_string.encode(),
hashlib.sha256
).hexdigest()
return {
"X-Timestamp": timestamp,
"X-Signature": signature,
}
# 使用示例
api_secret = b"sk_hmac_9f3a2b7c4d8e1a6f"
body = json.dumps({"customer_id": "cust_4421", "action": "suspend_account"})
headers = sign_request(api_secret, "POST", "/api/v2/customers/actions", body)
print(headers)
# {"X-Timestamp": "1711612200", "X-Signature": "a3f1b9c0..."}使用 requests 库发送签名 HTTP 请求
import hmac
import hashlib
import time
import json
import requests
API_SECRET = b"sk_hmac_9f3a2b7c4d8e1a6f"
BASE_URL = "https://api.billing-service.internal"
def make_signed_request(method: str, path: str, payload: dict) -> requests.Response:
body = json.dumps(payload, separators=(",", ":")) # 紧凑 JSON,确定性
timestamp = str(int(time.time()))
signing_string = f"{method}\n{path}\n{timestamp}\n{body}"
signature = hmac.new(API_SECRET, signing_string.encode(), hashlib.sha256).hexdigest()
headers = {
"Content-Type": "application/json",
"X-Timestamp": timestamp,
"X-Signature": f"hmac-sha256={signature}",
}
try:
return requests.request(method, f"{BASE_URL}{path}", data=body, headers=headers)
except requests.RequestException as e:
raise RuntimeError(f"Signed request failed: {e}") from e
# 发送签名 POST 请求
resp = make_signed_request("POST", "/api/v2/invoices", {
"customer_id": "cust_4421",
"line_items": [
{"description": "Pro plan - March 2026", "amount": 4900},
{"description": "Extra seats (3)", "amount": 2100},
],
})
print(resp.status_code, resp.json())注意:序列化请求体用于签名时,使用 separators=(",", ":")。 默认 json.dumps() 在分隔符后添加空格,若服务端序列化方式不同则会改变字节表示,导致签名验证失败。紧凑 JSON 提供了规范形式。
命令行 HMAC 生成
有时需要在不编写脚本的情况下计算 HMAC。Python 的 -c 标志和 openssl 都可以在终端完成此操作。
python3 -c " import hmac, hashlib print(hmac.new(b'my_secret', b'message_to_sign', hashlib.sha256).hexdigest()) " # 输出:64 字符十六进制字符串
echo -n "message_to_sign" | openssl dgst -sha256 -hmac "my_secret" # SHA2-256(stdin)= 7d11... # 通过 openssl HMAC 处理文件 openssl dgst -sha256 -hmac "my_secret" < payload.json
# 将密钥存入环境变量以避免暴露在 shell 历史记录中
export HMAC_KEY="sk_live_9f3a2b"
echo -n '{"event":"test"}' | python3 -c "
import hmac, hashlib, sys, os
key = os.environ['HMAC_KEY'].encode()
msg = sys.stdin.buffer.read()
print(hmac.new(key, msg, hashlib.sha256).hexdigest())
"echo -n 标志至关重要——不加此标志,echo 会在消息末尾附加换行符,从而改变 HMAC 输出。这是从终端调试时签名不匹配最常见的原因。高性能替代方案——cryptography 库
对于大多数应用,标准 hmac 模块已足够快速。如果你已经使用 cryptography 库进行 TLS 或证书处理,它也提供基于 OpenSSL 的 HMAC 实现。与标准库的主要实际区别在于下面介绍的基于异常的 .verify() API——不匹配时抛出异常,而非返回一个可能被忽略的布尔值。
pip install cryptography
from cryptography.hazmat.primitives import hashes, hmac as crypto_hmac
key = b"webhook_signing_key_2026"
message = b'{"event":"subscription.renewed","plan":"enterprise"}'
h = crypto_hmac.HMAC(key, hashes.SHA256())
h.update(message)
signature = h.finalize() # 原始字节
print(signature.hex())
# "9c4e2a..."
# 验证模式——不匹配时抛出 InvalidSignature
h_verify = crypto_hmac.HMAC(key, hashes.SHA256())
h_verify.update(message)
h_verify.verify(signature) # 若签名错误则抛出 cryptography.exceptions.InvalidSignaturecryptography 库的 .verify() 方法特别实用:不匹配时抛出异常而非返回布尔值,使得验证失败更难被意外忽略。标准库的 hmac.compare_digest() 返回 True/ False, 若开发者忘记检查返回值,可能被静默忽略。
带语法高亮的终端输出
如果在终端调试 HMAC 签名并希望彩色输出, rich 可以很好地处理这一需求。
pip install rich
import hmac
import hashlib
from rich.console import Console
from rich.table import Table
console = Console()
key = b"debug_signing_key"
messages = {
"/api/v2/orders": b'{"status":"active"}',
"/api/v2/invoices": b'{"status":"pending"}',
"/api/v2/customers": b'{"status":"verified"}',
}
table = Table(title="HMAC-SHA256 签名")
table.add_column("接口", style="cyan")
table.add_column("签名(前 32 个字符)", style="green")
for endpoint, body in messages.items():
sig = hmac.new(key, body, hashlib.sha256).hexdigest()
table.add_row(endpoint, sig[:32] + "...")
console.print(table)处理大文件——增量 HMAC
对于 50 MB 以上的文件,为计算 HMAC 而将所有内容加载到内存是一种浪费。HMAC 对象的 .update() 方法允许分块输入数据,无论文件大小如何,内存用量保持恒定。
import hmac
import hashlib
def hmac_file(key: bytes, filepath: str, chunk_size: int = 8192) -> str:
"""计算文件的 HMAC-SHA256,无需将整个文件加载到内存。"""
h = hmac.new(key, digestmod=hashlib.sha256)
try:
with open(filepath, "rb") as f:
while True:
chunk = f.read(chunk_size)
if not chunk:
break
h.update(chunk)
except OSError as e:
raise OSError(f"Cannot read file '{filepath}': {e}") from e
return h.hexdigest()
# 对 2 GB 数据库导出文件签名
signing_key = b"backup_integrity_key_2026"
signature = hmac_file(signing_key, "/var/backups/db-export-2026-03.sql.gz")
print(f"HMAC-SHA256: {signature}")import hmac
import hashlib
def verify_file_hmac(key: bytes, filepath: str, expected_hex: str) -> bool:
"""验证文件的 HMAC-SHA256 签名。"""
h = hmac.new(key, digestmod=hashlib.sha256)
with open(filepath, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
h.update(chunk)
return hmac.compare_digest(h.hexdigest(), expected_hex)
# 验证已下载的产物
is_valid = verify_file_hmac(
key=b"release_signing_key",
filepath="/tmp/release-v3.2.0.tar.gz",
expected_hex="8e3a1f9b2c4d6e7f0a1b3c5d7e9f0a2b4c6d8e0f1a2b3c4d5e6f7a8b9c0d1e2f",
)
print(f"文件完整性:{'有效' if is_valid else '已损坏'}").update() 方式使用固定的 chunk_size 内存,与文件大小无关。 默认使用 64 KB 块——足以分摊系统调用开销,又足够小以留在大多数硬件的 L2 缓存中。在 Python 中生成密码学安全的 HMAC 密钥
弱密钥或可预测密钥会破坏整个 HMAC 构造。 secrets 模块(Python 3.6+)提供密码学强度的随机字节。 对于 HMAC-SHA256,使用 32 字节密钥。对于 HMAC-SHA512,使用 64 字节。这些长度与各自哈希算法的内部块大小匹配,是 RFC 2104 规定的最佳密钥长度。
import secrets
# 生成与哈希算法块大小匹配的密钥
sha256_key = secrets.token_bytes(32) # 256 位——用于 HMAC-SHA256
sha512_key = secrets.token_bytes(64) # 512 位——用于 HMAC-SHA512
# 十六进制表示——适合配置文件和环境变量
print(f"HMAC-SHA256 key: {sha256_key.hex()}")
# 例如 "a3f1b9c04e7d2f8a1b3c5d7e9f0a2b4c6d8e0f1a2b3c4d5e6f7a8b9c0d1e2f"
print(f"HMAC-SHA512 key: {sha512_key.hex()}")
# 128 字符十六进制字符串
# URL 安全 Base64——紧凑,适合 HTTP 头部
import base64
b64_key = base64.urlsafe_b64encode(sha256_key).decode("ascii")
print(f"Base64 key: {b64_key}")
# 例如 "o_G5wE59L4obPF1-nwortG2ODwobPExdXnqLnA0dLi8="random 模块的 random.random() 或 random.randbytes() 生成 HMAC 密钥。默认 random 模块使用 Mersenne Twister 伪随机数生成器,在观察到 624 个输出后可预测。安全敏感的随机性始终使用 secrets.token_bytes()。密钥长度与 RFC 2104 要求
RFC 2104 规定 HMAC 密钥可以是任意长度,但建议至少 L 字节——其中 L 是底层哈希函数的输出长度。对于 HMAC-SHA256,即 32 字节(256 位)。短于 L 位的密钥会按比例降低安全余量。长于哈希块大小的密钥(SHA-256 为 64 字节,SHA-512 为 128 字节)会先被哈希到块大小,因此使用比块大小更长的密钥没有任何好处。HMAC-SHA256 使用 32 字节,HMAC-SHA512 使用 64 字节即可。
安全密钥存储与轮换
永远不要将 HMAC 密钥硬编码在源代码中。生产部署的标准做法是在启动时从环境变量加载密钥: os.environ["HMAC_SECRET"].encode()。 对于更高安全要求的环境,将密钥存储在 AWS Secrets Manager、HashiCorp Vault 或 GCP Secret Manager 等密钥管理系统中,并在运行时获取。这些系统提供审计日志、访问控制和自动轮换,无需重新部署代码。
从一开始就为密钥轮换做好规划。轮换密钥时,存在一个过渡窗口:已用旧密钥签名的在途请求在新密钥下验证会失败。标准的缓解方案是短暂的重叠期:在短时间内(几分钟到几小时)同时接受新旧密钥的签名,然后停用旧密钥。如果密钥被泄露——出现在日志中、通过 git 提交泄露或在安全事件中暴露——立即轮换,并将所有用泄露密钥生成的签名视为不可信。重新验证所有缓存的验证结果,并通知下游消费方密钥已更换。
在 hmac.new() 中使用 bytearray 和 memoryview
hmac.new() 函数的 key 和 msg 参数接受任何字节类对象。这意味着可以直接传入 bytes、 bytearray 或 memoryview, 无需复制或转换。这在两种场景下最为重要:网络协议实现( socket.recv_into() 将数据写入预分配的 bytearray 缓冲区),以及高吞吐量系统(避免中间复制以降低 GC 压力)。 memoryview 切片是零拷贝的:它暴露原始缓冲区的一个窗口而不分配新内存。每秒处理数万条消息时,消除这些内存分配对延迟和吞吐量有显著影响。
import hmac
import hashlib
# bytearray——可变字节,适用于二进制协议缓冲区
key = bytearray(b"protocol_signing_key")
frame = bytearray(b'\x01\x02\x03\x04payload_data_here')
sig = hmac.new(key, frame, hashlib.sha256).hexdigest()
print(f"Frame signature: {sig[:32]}...")
# memoryview——较大缓冲区的零拷贝切片
large_buffer = bytearray(4096)
large_buffer[:20] = b"sensor_reading_12345"
# 仅对前 20 字节计算 HMAC,无需复制
view = memoryview(large_buffer)[:20]
sig = hmac.new(key, view, hashlib.sha256).hexdigest()
print(f"Sensor signature: {sig[:32]}...")常见错误
前两个错误几乎出现在每一次涉及 Webhook 处理程序的代码审查中。在时间压力下很容易引入,在不知道要找什么的情况下很难发现。
问题: == 运算符在第一个字节不匹配时短路,泄露时序信息,让攻击者得以逐步重建预期签名。
解决方法: 始终使用 hmac.compare_digest() 进行签名比较——无论不匹配发生在何处,它都以恒定时间运行。
received_sig = request.headers["X-Signature"]
expected_sig = hmac.new(key, body, hashlib.sha256).hexdigest()
if received_sig == expected_sig: # 存在时序攻击漏洞
process_webhook(body)received_sig = request.headers["X-Signature"]
expected_sig = hmac.new(key, body, hashlib.sha256).hexdigest()
if hmac.compare_digest(received_sig, expected_sig): # 恒定时间
process_webhook(body)问题: hmac.new() 需要字节类对象。传入 Python str 会抛出 TypeError: "key: expected bytes or bytearray, but got 'str'"。
解决方法: 在将字符串密钥和消息传入 hmac.new() 之前调用 .encode()。
key = "my_api_secret" # str,不是 bytes
msg = '{"event":"test"}' # str,不是 bytes
sig = hmac.new(key, msg, hashlib.sha256) # TypeError!key = "my_api_secret"
msg = '{"event":"test"}'
sig = hmac.new(key.encode(), msg.encode(), hashlib.sha256)问题: 不传第三个参数调用 hmac.new(key, msg) 会抛出 TypeError。Python 3.4 之前默认为 MD5,但出于安全原因删除了该默认值。
解决方法: 始终明确传入算法:hashlib.sha256、hashlib.sha512,或协议所要求的其他算法。
# 缺少 digestmod——Python 3.4+ 中抛出 TypeError sig = hmac.new(key, msg).hexdigest()
# 始终指定哈希算法 sig = hmac.new(key, msg, hashlib.sha256).hexdigest()
问题: 许多 Webhook 提供商(Stripe、Shopify)发送 Base64 编码的签名,而非十六进制。将十六进制字符串与 Base64 值比较始终会失败,导致所有 Webhook 被拒绝。
解决方法: 查阅提供商文档确认签名格式。如果使用 Base64,对原始 .digest() 字节编码,而非对 .hexdigest() 字符串编码。
# 提供方发送 Base64,但我们计算十六进制——永远不会匹配 expected = hmac.new(key, body, hashlib.sha256).hexdigest() # "a3f1b9c0..." vs "o/G5wE59..." — 始终不匹配
import base64
# 与提供方格式匹配:原始字节 → Base64
raw = hmac.new(key, body, hashlib.sha256).digest()
expected = base64.b64encode(raw).decode("ascii")
# "o/G5wE59..." — 与头部匹配标准库 hmac 与 cryptography——快速对比
Webhook 验证、API 签名和常规 HMAC 操作使用标准库 hmac 模块——零依赖,涵盖所有标准算法。批量操作中一次性速度很重要时,使用 hmac.digest()。 只有在已因其他用途(TLS、X.509、对称加密)依赖 cryptography 库,并希望使用基于异常的 .verify() API 时,才考虑使用它。如需不编写任何 Python 代码快速校验签名,使用 HMAC 生成器工具 粘贴密钥和消息,即刻获得结果。
常见问题
Python 中 hmac.new() 和 hmac.digest() 有什么区别?
hmac.new() 返回一个 HMAC 对象,支持增量 .update() 调用,并提供 .digest()(原始字节)和 .hexdigest()(十六进制字符串)两种输出方式。hmac.digest() 是 Python 3.7 新增的一次性函数,直接返回原始字节,无需创建中间对象。当消息完整且只需要结果时,使用 hmac.digest()。当需要分块输入数据或需要十六进制输出时,使用 hmac.new()。
import hmac, hashlib
key = b"webhook_secret_2026"
body = b'{"event":"payment.completed","amount":9950}'
# 一次性——返回原始字节
raw = hmac.digest(key, body, hashlib.sha256)
# 基于对象——支持增量更新和十六进制输出
h = hmac.new(key, body, hashlib.sha256)
hex_sig = h.hexdigest()如何在 Python 中验证 HMAC 签名?
使用共享密钥对原始消息重新计算 HMAC,然后使用 hmac.compare_digest() 进行比较。永远不要用 == 进行签名比较。== 运算符在第一个不匹配的字节处就会短路,泄露有关预期签名长度和内容的信息,从而面临时序攻击风险。
import hmac, hashlib
def verify_signature(secret: bytes, message: bytes, received_sig: str) -> bool:
expected = hmac.new(secret, message, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, received_sig)Python 的 HMAC-SHA256 与 hashlib.sha256 哈希相同吗?
不同。hashlib.sha256 计算输入的普通 SHA-256 哈希,任何人都可以重现。HMAC-SHA256 按照 RFC 2104 将密钥混入哈希计算,因此只有持有密钥的人才能生成或验证正确的输出。普通哈希证明数据完整性,HMAC 同时证明完整性和真实性。
import hmac, hashlib msg = b"transfer:9950:USD" key = b"api_secret_k8x2" plain_hash = hashlib.sha256(msg).hexdigest() # 任何人都能计算 hmac_hash = hmac.new(key, msg, hashlib.sha256).hexdigest() # 需要密钥
在 Python 3 中可以使用 HMAC-SHA1 吗?
可以,将 hashlib.sha1 作为 digestmod 参数传入即可。HMAC-SHA1 在 Python 3 中仍然可以正常工作,hmac 模块对此没有弃用警告。但 SHA-1 在新设计中被认为较弱——其碰撞抗性低于 80 位,NIST 于 2015 年将其大多数数字签名用途列为弃用。当今使用 HMAC-SHA1 的主要原因是与现有协议的向后兼容性,如 OAuth 1.0a 或某些旧版 Webhook 系统。当你控制集成双方时,所有新工作应优先选用 HMAC-SHA256 或 HMAC-SHA512。
import hmac, hashlib key = b"oauth_consumer_secret" base_string = b"GET&https%3A%2F%2Fapi.example.com&oauth_nonce%3Dabc123" sig = hmac.new(key, base_string, hashlib.sha1).digest()
如何在 Python 中生成安全的 HMAC 密钥?
使用标准库中的 secrets.token_bytes()。对于 HMAC-SHA256,推荐使用 32 字节密钥,因为这与哈希块大小匹配。对于 HMAC-SHA512,使用 64 字节。不要在应用代码中使用 random.random() 或 os.urandom() 生成密钥——自 Python 3.6 起,secrets 是安全敏感随机性的正确模块。
import secrets hmac_sha256_key = secrets.token_bytes(32) # 256 位 hmac_sha512_key = secrets.token_bytes(64) # 512 位 # 以十六进制存储用于配置文件 print(hmac_sha256_key.hex()) # 例如 "a3f1b9c04e..."
为什么 Python 3 中 hmac.new() 需要 digestmod 参数?
Python 3.4 之前,digestmod 默认为 MD5,而 MD5 在密码学上已被破解——MD5 存在已知碰撞攻击,绝不应在新的安全敏感代码中使用。Python 维护者删除了默认值,以强制开发者明确选择算法,防止无意间部署基于 MD5 的 MAC。如果不传 digestmod 调用 hmac.new(key, msg),会抛出 TypeError。始终明确传入算法:hashlib.sha256、hashlib.sha512 或其他 hashlib 构造函数。不确定时,hashlib.sha256 是安全的默认选择——无已知弱点,且对任何实际工作负载都足够快。
import hmac, hashlib key = b"secret" msg = b"data" # Python 3.4+ 中会抛出 TypeError # hmac.new(key, msg) # TypeError: missing required argument: 'digestmod' # 始终明确指定算法 h = hmac.new(key, msg, hashlib.sha256)
如需在不运行 Python 脚本的情况下快速校验 HMAC,将密钥和消息粘贴到 在线 HMAC 生成器 中——支持 SHA-256、SHA-384 和 SHA-512,结果即时可得。
相关工具
Dmitri is a DevOps engineer who relies on Python as his primary scripting and automation language. He builds internal tooling, CI/CD pipelines, and infrastructure automation scripts that run in production across distributed teams. He writes about the Python standard library, subprocess management, file processing, encoding utilities, and the practical shell-adjacent Python that DevOps engineers use every day.
Maria is a backend developer specialising in Python and API integration. She has broad experience with data pipelines, serialisation formats, and building reliable server-side services. She is an active member of the Python community and enjoys writing practical, example-driven guides that help developers solve real problems without unnecessary theory.