极简工具箱

免费在线工具 · 本地计算 · 无需上传

HMAC 是什么:为什么签名要带密钥,不能直接哈希

接口签名里常见 `sign = HMAC-SHA256(secret, data)`。为什么不能简单写成 `sha256(secret + data)`?答案藏在一个叫「长度扩展攻击」的细节里。

广告位(百度联盟)

一、HMAC 解决什么问题

普通哈希能证明「内容没变」,但任何人都能重新算一遍——攻击者改了内容,自己算个新哈希附上就行。HMAC(Hash-based Message Authentication Code) 引入一个只有双方知道的密钥:

HMAC(K, M) = H( (K' ⊕ opad) || H( (K' ⊕ ipad) || M ) )

看不懂公式没关系,记住三点:

二、为什么不能自己拼 hash(key + msg)

这种写法叫「前缀密钥哈希」,存在长度扩展攻击:已知 H(key || msg) 和 key 的长度,攻击者可以在不知道 key 的情况下,构造出 H(key || msg || padding || 追加内容) 的合法值。因为 Merkle–Damgård 结构的哈希(MD5、SHA-1、SHA-256)可以从中途状态继续计算。

HMAC 用两层哈希(ipad/opad)把这个口子堵死。所以:要带密钥的摘要,用标准 HMAC,不要自己拼字符串。

三、接口签名的典型流程

  1. 双方约定一个 secret(通过安全渠道下发,不进前端代码)。
  2. 把业务参数按 key 字典序排序,拼成 k1=v1&k2=v2(空值是否参与要写进文档)。
  3. 加上时间戳 timestamp 和随机串 nonce。
  4. 对整串计算 HMAC-SHA256,输出十六进制或 Base64。
  5. 服务端用同样规则重算,比对一致且时间戳在允许窗口内(如 ±5 分钟)才放行。
import hmac, hashlib, base64
sig = base64.b64encode(
    hmac.new(secret.encode(), msg.encode(), hashlib.sha256).digest()
).decode()
const crypto = require('crypto');
const sig = crypto.createHmac('sha256', secret).update(msg).digest('hex');

四、四个常见实现错误

五、HMAC 与其它机制的区别

机制密钥用途
普通哈希无校验完整性(无攻击者场景)
HMAC对称密钥接口签名、Webhook 校验
RSA/ECDSA 签名私钥签、公钥验多方场景,验签方不需持有签名密钥
AES-GCM对称密钥加密 + 完整性一体

Webhook 场景(如支付回调)几乎都用 HMAC:提供方给一个 secret,接收方对原始请求体算 HMAC 并比对。注意要用原始字节算,不要先 JSON 解析再序列化——顺序和空白变了,签名就对不上。

六、调试方法

签名对不上时,把「待签名字符串」原样打印出来,两边对照,通常一眼就能看出少了个参数或编码不同。用 HMAC 生成工具 可以快速验证某个 key+message 的期望输出,再回去比对代码。需要把二进制签名放进 JSON 或 URL 时,用 Base64 编解码 转换。

小结

HMAC = 带密钥的哈希,提供完整性与身份认证;不要自己拼 hash(key+msg);签名必须配时间戳和随机串防重放;比较要用常量时间函数。

本文相关工具(本地计算,无需上传)

更多使用教程