Python HMAC hmac.new() SHA-256

·DevOps Engineer & Python Automation Specialist·审阅者Maria Santos·发布日期

直接在浏览器中使用免费的 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 规范。

Before · Python
After · Python
# 普通 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"

Python 3.7+ — 最简 HMAC-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——无需解码步骤。

Python 3.7+ — .digest() 与 .hexdigest()
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 模块会拒绝它们。

Python 3.7+ — 将字符串编码为字节
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() 即可。

Python 3.7+ — HMAC-SHA256 Base64 编码
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 实现所要求。代码完全相同——只需换用算法即可。

Python 3.7+ — HMAC-SHA1
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 — 更长输出

Python 3.7+ — 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 — 仅用于旧系统

Python 3.7+ — 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-MD5 仅在无法迁移的旧系统中才可接受。对于任何新项目,至少使用 HMAC-SHA256。MD5 存在已知碰撞漏洞,尽管在 HMAC 模式下不那么容易被直接利用,但仍是较差的默认选择。

hmac.new() 参数参考

构造函数签名为 hmac.new(key, msg=None, digestmod)。 模块中的三个主要函数对密钥和算法参数使用相同模式。

hmac.new() 构造函数

参数
类型
默认值
说明
key
bytes | bytearray
(必填)
密钥——必须是字节,不能是字符串
msg
bytes | None
None
初始消息;可通过 .update() 追加更多数据
digestmod
str | callable
(必填)
哈希算法——如 hashlib.sha256 或字符串 "sha256"

hmac.digest() 一次性函数(Python 3.7+)

参数
类型
说明
key
bytes
密钥
msg
bytes
要认证的消息
digest
str | callable
哈希算法——与 hmac.new() 中的 digestmod 相同

digestmod 参数接受可调用对象(如 hashlib.sha256) 或字符串名称(如 "sha256")。 推荐使用可调用形式,因为它在导入时即被验证——字符串形式中的拼写错误只在运行时才会报错。

hmac.digest() — 快速一次性 HMAC(Python 3.7+)

Python 3.7 新增了 hmac.digest(key, msg, digest) 作为模块级函数。它在一次调用中完成 HMAC 计算,无需创建中间 HMAC 对象。返回值是原始字节(等同于对对象调用 .digest())。 该函数在 CPython 上使用优化的 C 实现,避免了对象分配开销,在频繁调用的场景中速度明显更快。

Python 3.7+ — hmac.digest() 一次性调用
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 签名验证

Python 3.7+ — Webhook HMAC 验证(Flask)
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 "", 200

hmac.compare_digest() 函数以恒定时间比较两个字符串或字节序列。普通的 == 比较在第一个不匹配字节处短路。攻击者可以通过多次请求测量响应时间,逐字节重建预期签名。恒定时间比较消除了这一旁信道。

GitHub Webhook 验证

GitHub 的 Webhook 格式展示了完整的模式。它发送一个 X-Hub-Signature-256 头部,内容为 sha256= 后跟原始请求体的十六进制 HMAC-SHA256,使用你在仓库设置中配置的 Webhook 密钥签名。与通用 Webhook 验证的关键区别在于:比较前必须去掉 sha256= 前缀,且必须读取请求体的原始字节——先解析 JSON 会改变字节表示,导致验证失败。

Python 3.7+ — GitHub X-Hub-Signature-256 验证
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 方法、路径、时间戳和请求体。以下是用于服务间内部认证的模式。

Python 3.7+ — 使用 HMAC-SHA256 对 API 请求签名
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 请求

Python 3.7+ — 使用 requests 库发送 HMAC 签名请求
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 都可以在终端完成此操作。

bash — 通过 Python 单行命令计算 HMAC-SHA256
python3 -c "
import hmac, hashlib
print(hmac.new(b'my_secret', b'message_to_sign', hashlib.sha256).hexdigest())
"
# 输出:64 字符十六进制字符串
bash — 通过 openssl 计算 HMAC-SHA256(用于对比)
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
bash — 从环境变量读取密钥计算 HMAC
# 将密钥存入环境变量以避免暴露在 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——不匹配时抛出异常,而非返回一个可能被忽略的布尔值。

bash — 安装 cryptography
pip install cryptography
Python 3.7+ — 通过 cryptography 库使用 HMAC
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.InvalidSignature

cryptography 库的 .verify() 方法特别实用:不匹配时抛出异常而非返回布尔值,使得验证失败更难被意外忽略。标准库的 hmac.compare_digest() 返回 True/ False, 若开发者忘记检查返回值,可能被静默忽略。

带语法高亮的终端输出

如果在终端调试 HMAC 签名并希望彩色输出, rich 可以很好地处理这一需求。

bash — 安装 rich
pip install rich
Python 3.7+ — 使用 rich 输出彩色 HMAC 结果
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)
注意:rich 仅用于终端显示。不要在将 HMAC 签名写入文件、HTTP 头部或日志聚合系统时使用它——ANSI 转义码会破坏输出。

处理大文件——增量 HMAC

对于 50 MB 以上的文件,为计算 HMAC 而将所有内容加载到内存是一种浪费。HMAC 对象的 .update() 方法允许分块输入数据,无论文件大小如何,内存用量保持恒定。

Python 3.7+ — 分块计算大文件 HMAC
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}")
Python 3.7+ — 验证已下载文件的 HMAC 签名
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 '已损坏'}")
注意:当文件超过 50–100 MB 或在 Web 服务器中处理上传文件、每个请求的内存用量很重要时,切换到分块 HMAC。 .update() 方式使用固定的 chunk_size 内存,与文件大小无关。 默认使用 64 KB 块——足以分摊系统调用开销,又足够小以留在大多数硬件的 L2 缓存中。

在 Python 中生成密码学安全的 HMAC 密钥

弱密钥或可预测密钥会破坏整个 HMAC 构造。 secrets 模块(Python 3.6+)提供密码学强度的随机字节。 对于 HMAC-SHA256,使用 32 字节密钥。对于 HMAC-SHA512,使用 64 字节。这些长度与各自哈希算法的内部块大小匹配,是 RFC 2104 规定的最佳密钥长度。

Python 3.7+ — 使用 secrets 生成 HMAC 密钥
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 切片是零拷贝的:它暴露原始缓冲区的一个窗口而不分配新内存。每秒处理数万条消息时,消除这些内存分配对延迟和吞吐量有显著影响。

Python 3.7+ — 在 HMAC 中使用 bytearray 和 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() 比较 HMAC 签名

问题: == 运算符在第一个字节不匹配时短路,泄露时序信息,让攻击者得以逐步重建预期签名。

解决方法: 始终使用 hmac.compare_digest() 进行签名比较——无论不匹配发生在何处,它都以恒定时间运行。

Before · Python
After · Python
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() 传入字符串而非字节

问题: hmac.new() 需要字节类对象。传入 Python str 会抛出 TypeError: "key: expected bytes or bytearray, but got 'str'"。

解决方法: 在将字符串密钥和消息传入 hmac.new() 之前调用 .encode()。

Before · Python
After · Python
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)
忘记 digestmod(Python 3.4+)

问题: 不传第三个参数调用 hmac.new(key, msg) 会抛出 TypeError。Python 3.4 之前默认为 MD5,但出于安全原因删除了该默认值。

解决方法: 始终明确传入算法:hashlib.sha256、hashlib.sha512,或协议所要求的其他算法。

Before · Python
After · Python
# 缺少 digestmod——Python 3.4+ 中抛出 TypeError
sig = hmac.new(key, msg).hexdigest()
# 始终指定哈希算法
sig = hmac.new(key, msg, hashlib.sha256).hexdigest()
提供方期望 Base64 时却使用 .hexdigest()

问题: 许多 Webhook 提供商(Stripe、Shopify)发送 Base64 编码的签名,而非十六进制。将十六进制字符串与 Base64 值比较始终会失败,导致所有 Webhook 被拒绝。

解决方法: 查阅提供商文档确认签名格式。如果使用 Base64,对原始 .digest() 字节编码,而非对 .hexdigest() 字符串编码。

Before · Python
After · Python
# 提供方发送 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——快速对比

方法
算法
输出
流式处理
自定义类型
需要安装
hmac.new() + hexdigest()
任意 hashlib
十六进制字符串
✓ 通过 .update()
N/A
否(标准库)
hmac.new() + digest()
任意 hashlib
原始字节
✓ 通过 .update()
N/A
否(标准库)
hmac.digest()
任意 hashlib
原始字节
✗(一次性)
N/A
否(标准库,3.7+)
hashlib.sha256(普通哈希)
仅 SHA-256
十六进制或字节
✓ 通过 .update()
N/A
否(标准库)
cryptography(HMAC)
任意
原始字节
✓ 通过 .update()
N/A
pip install
pyca/cryptography + CMAC
AES-CMAC
原始字节
✓ 通过 .update()
N/A
pip install

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()。

Python
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() 进行比较。永远不要用 == 进行签名比较。== 运算符在第一个不匹配的字节处就会短路,泄露有关预期签名长度和内容的信息,从而面临时序攻击风险。

Python
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 同时证明完整性和真实性。

Python
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。

Python
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 是安全敏感随机性的正确模块。

Python
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 是安全的默认选择——无已知弱点,且对任何实际工作负载都足够快。

Python
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,结果即时可得。

相关工具

DV
Dmitri VolkovDevOps Engineer & Python Automation Specialist

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.

MS
Maria Santos技术审阅者

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.