如何计算 Base64 格式的 HMAC
- 已选中 HMAC 模式和 SHA-256。如果你的 API 使用 SHA-1、SHA-384、SHA-512 或 MD5,请在 算法 中另选。
- 输入 密钥,并设置 密钥格式:普通的密钥字符串选文本(UTF-8),密钥以编码字节形式给出时选 Hex 或 Base64。
- 在 文本 中输入或粘贴消息,或把 输入 切换为 文件,拖入确切的载荷文件。
- 以 Base64、Base64URL 或 hex 复制签名。所有算法 会同时显示每种哈希的 HMAC,文档没说清用的是哪种时很有帮助。
空密钥也能接受并得出结果,同时会提示这样的 HMAC 起不到任何保护作用。
示例值
密钥为 key,消息为 The quick brown fox jumps over the lazy dog 时:
| 算法 | Base64 格式的 HMAC |
|---|---|
| HMAC-MD5 | gAcHE0Y+d0m5DC3CSRHidQ== |
| HMAC-SHA1 | 3nybhbi3iqa8ino29wqQcBydtNk= |
| HMAC-SHA256 | 97yD9DBThCSxMpjmqm+xQ+9NWaFJRhdZl0edvC0aPNg= |
| HMAC-SHA512 | tCrwkFe6weLUFwjkipAuCbX/fxKrQopP6GZTxz3SSPuC+UilSfe3kaW0GRXuTR7Dk1NX5OIxclDQNyr6Lr7rOg== |
HMAC-SHA256 的 hex 值是 f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8,也就是维基百科 HMAC 词条中那个熟悉的测试向量。密钥的编码方式会改变结果:把文本 a2V5 用作密钥得到 5O+7AtpguFrgIRw1qXGU+m0OaTs1oV1kwRQ/hI1WAUE=,而把 a2V5 按 Base64 读取,得到的是 key 的字节,结果又是 97yD9DBT…。
Webhook 签名:Base64 还是 hex
各服务方都用 HMAC,但输出编码和签名内容并不统一。几个文档齐全的例子:
| 服务方 | 请求头 | 算法 | 编码 | 签名内容 |
|---|---|---|---|---|
| Shopify | X-Shopify-Hmac-Sha256 | HMAC-SHA256 | Base64 | 原始请求体 |
| Standard Webhooks(Svix) | webhook-signature: v1,… | HMAC-SHA256 | Base64 | id.timestamp.body,密钥取自 whsec_ 后的 Base64 |
| Twilio | X-Twilio-Signature | HMAC-SHA1 | Base64 | URL 加上排序后的 POST 参数 |
| GitHub | X-Hub-Signature-256: sha256=… | HMAC-SHA256 | hex | 原始请求体 |
| Stripe | Stripe-Signature: t=…,v1=… | HMAC-SHA256 | hex | timestamp.body |
如果本工具算出的值与 hex 一致,而你的代码比较的是 Base64(或者反过来),说明计算没错,只是编码不同。签名内容请查阅服务方的文档;时间戳前缀是为了防止旧请求被重放。
在代码中验证签名
用恒定时间比较原始字节,并确保对收到的请求体原样计算哈希:
const crypto = require("crypto");
function verifyWebhook(rawBody, secret, signatureHeader) {
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest();
const given = Buffer.from(signatureHeader, "base64");
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
}
把狐狸那句话的 Buffer 作为 rawBody,密钥为 key,再传入表中的 Base64 值,它返回 true。如果传入的是 JSON.stringify(JSON.parse(body)) 而不是原始请求体,只要服务方的 JSON 里有一个你的序列化器会丢掉的空格,它就返回 false。在 Express 中,这意味着要在 Webhook 路由上用 express.raw() 读取请求体。
Python 中的对应写法是用 hmac.new(key, body, hashlib.sha256).digest(),对结果做 Base64 编码,再用 hmac.compare_digest 比较。命令行中,openssl dgst -sha256 -hmac 'key' -binary | base64 会得到同样的 Base64;密钥是 hex 时请用 -mac HMAC -macopt hexkey:…。要生成强密钥,可以用 Base64 密钥生成器 的 HMAC 密钥预设,它会生成 32 个随机字节。如果要签名的是令牌而不是请求,请看 JWT 编码器。
常见问题
为什么我的 Webhook HMAC 对不上?
签名覆盖的是请求体的确切字节。如果框架已经解析了 JSON,而你对重新序列化后的副本计算哈希,空格或键的顺序会变化,结果就不同。请对原始请求体计算哈希,确认服务方要求的是 hex 还是 Base64,并确认密钥是否需要先解码。
HMAC 密钥是文本还是 Base64?
取决于服务方。大多数给你的是一个按 UTF-8 文本使用的字符串。有些提供的是 Base64 编码的密钥,有时带有 whsec_ 之类的前缀,使用前必须解码为字节。请相应地设置 密钥格式。
哈希和 HMAC 有什么区别?
任何人都能计算一条消息的 SHA-256,所以哈希只能发现意外的改动。HMAC 混入了密钥,只有持有密钥的人才能生成有效的值,因此它是一种签名。
可以使用 HMAC-MD5 或 HMAC-SHA1 吗?
两者仍在一些老 API 中使用,而且比单纯的 MD5 或 SHA-1 强得多,因为碰撞并不能转化为 HMAC 伪造。新设计请选择 HMAC-SHA256。