首页 / 资讯中心 / 文章详情

HMAC-SHA256消息认证码详解:从算法原理到安全签名实践

HMAC-SHA256消息认证码详解:从算法原理到安全签名实践 ★ FEATURED ARTICLE
一次接口联调对方把签名规则丢过来参数按字典序拼接用 HMAC-SHA256 加密密钥是双方约定好的一串字符。我盯着文档里的HMAC-SHA256愣了一下加密后的串是 64 位十六进制字母跟网页里常见的那种MD5 加密完全不是一个东西手工根本算不出来。当时急着联调第一反应就是找个在线工具快速验证签名串对不对。网上一搜工具一大堆但能放心用的没几个有个页面甚至把密钥填进去之后右下角还在悄悄发网络请求。从那以后我就把HMAC-SHA 是什么、怎么生成、工具怎么选这件事彻底捋清楚了这篇文章就把整个过程完整地写出来。要说清楚 HMAC-SHA必须先解释一个更基础的问题消息认证到底在防什么。很多人以为接口传输用了 HTTPS 就万事大吉其实 HTTPS 只解决传输过程被窃听或篡改的问题服务端收到请求后根本没法判断这串数据是不是真的来自合法客户端更没办法判断数据在到达之前有没有被改过。消息认证码MACMessage Authentication Code就是干这个的而 HMAC 是 MAC 里应用最广的一种实现方式。1. 为什么消息认证总比加密先出场一个签名对不上的排查经历先讲个真实场景。早些年我维护过一个内部数据同步服务两个系统之间通过 HTTP 接口互推数据。某天运营反馈某个渠道的订单金额偶尔对不上排查到最后发现是有人通过抓包工具截获了请求把订单金额字段改了之后再转发给服务端。服务端完全没有校验机制直接把篡改后的数据落库了。这就是典型的数据被篡改但接收方无法感知。后来我们加的方案就是 HMAC 签名客户端把请求参数按照固定规则拼接成字符串用约定的密钥做 HMAC-SHA256 运算把结果放在请求头的X-Signature字段里。服务端收到请求后用同样的密钥和同样的规则重新计算签名两个签名一致才继续处理。从那以后篡改请求这种操作就彻底失效了因为攻击者没有密钥算不出合法的签名。1.1 完整性与真实性的区别HMAC 到底保证了什么这里需要澄清一个概念。很多人把加密和签名混在一起实际上 HMAC 不属于加密算法它不隐藏任何信息原始数据还是明文传输HMAC 只负责给数据附加一个指纹。这个指纹有两层含义完整性数据在传输过程中有没有被改动过。哪怕只改了一个字节重新计算的 HMAC 值就会完全不同。真实性数据确实来自持有密钥的一方。因为 HMAC 的计算需要密钥没有密钥的人无法伪造合法的签名。但 HMAC 不负责防重放。攻击者虽然改不了数据却可以原封不动地把整条合法请求重复发送多次。所以实际项目中通常会在签名数据里加入时间戳或者随机数nonce服务端校验签名通过后还要检查时间戳是否在有效期内或者这个随机数是否已经用过双重验证才能有效防止重放攻击。1.2 对比几种防篡改方案的取舍不是只有 HMAC 一种方案能做消息认证常见的还有普通哈希加盐、数字签名等各有适用场景。我列个表格做一个直观对比方案计算方式密钥要求防篡改防伪造典型场景普通哈希SHA-256直接对数据做摘要无密钥能不能文件校验、密码存储哈希加盐数据拼接固定盐值后做摘要盐值需保密能弱简单接口校验易被逆向HMAC-SHA256密钥参与两次哈希运算密钥需保密能强API 签名、Webhook 回调校验RSA/ECDSA 数字签名私钥签名、公钥验证公私钥对能强开放平台、区块链交易普通哈希最大的问题是没有密钥参与任何人拿到数据和算法都能重新计算防篡改能力为零。哈希加盐相当于把盐值当成密钥用但实现不规范时很容易被暴力破解。数字签名安全级别最高但计算成本高、密钥管理复杂适合开放性场景。HMAC 是对称密钥体系的代表一把密钥解决校验问题计算速度快实现简单在内部系统、API 签名、支付回调这些场景里性价比最高。2. 拆开 HMAC 的壳密钥、哈希、填充之间的协作逻辑HMAC 的全称是 Hash-based Message Authentication Code基于哈希函数的消息认证码。它本质上做了一件事让密钥和消息以一种特殊的方式混合经过两次哈希计算最终得到一个固定长度的输出。这个输出随消息和密钥的变化而变化任何一方变了结果都会面目全非。2.1 为什么不能直接对密钥消息做一次哈希很多人第一反应是既然要防篡改那把密钥拼在消息前面直接算一次 SHA-256 不就行了比如sign sha256(secret message)。这个方案在简单场景下能用但存在一个著名的安全隐患叫长度扩展攻击Length Extension Attack。SHA-256 这类哈希算法是分组迭代计算的如果攻击者拿到了sha256(secret message)的结果并且知道 message 的长度他不需要知道 secret就可以在 message 后面追加新数据计算出sha256(secret message padding extra)的合法哈希值。也就是说攻击者可以扩展一条原本合法的消息并生成对应的新签名。对于secret message这种简单拼接方式这个攻击是真实可行的。HMAC 的两次哈希设计就是为了彻底堵住这个漏洞。2.2 RFC 2104 的标准流程ipad 与 opad 的设计精妙之处HMAC 的标准流程定义在 RFC 2104 中。首先需要明确一个参数哈希函数的分组长度 B。SHA-256 的 B 是 64 字节SHA-512 的 B 是 128 字节。处理密钥时有一个规则如果密钥长度小于 B在密钥尾部补零直到长度等于 B。如果密钥长度大于 B先对密钥做一次哈希用哈希结果作为密钥长度等于哈希输出长度必然小于 B。处理完密钥后定义两个固定值ipad 0x36 重复 B 次opad 0x5C 重复 B 次完整计算流程如下inner sha256((key 补零到 B 字节) XOR ipad) message hmac sha256((key 补零到 B 字节) XOR opad) inner简单来说第一步把密钥和 ipad 异或拼上消息做一次哈希得到内部摘要第二步把密钥和 opad 异或拼上内部摘要再做一次哈希得到最终结果。这两次哈希让输入和输出之间形成了复杂的依赖关系长度扩展攻击在 HMAC 结构下不再成立。我在实际理解时有个更直白的类比第一次哈希相当于把数据和密钥揉成一个面团第二次哈希相当于给这个面团再裹一层密钥做的脆皮。两次处理之后攻击者既无法从结果反推密钥也无法在不持有密钥的情况下构造出合法的新签名。2.3 一个最小实现示例用 Python 手工还原 HMAC 计算纸上谈兵终觉浅把算法一步步敲出来才能彻底理解。下面这个 Python 代码没有使用标准库的hmac模块而是根据 RFC 2104 的逻辑手工实现了一版import hashlib def hmac_sha256(key: bytes, msg: bytes) - bytes: block_size 64 # sha256 的分组长度 # 密钥长度大于分组长度时先哈希压缩 if len(key) block_size: key hashlib.sha256(key).digest() # 密钥长度小于分组长度时补零 key key.ljust(block_size, b\x00) # 构造 ipad 和 opad ipad bytes(0x36 for _ in range(block_size)) opad bytes(0x5c for _ in range(block_size)) inner hashlib.sha256(bytes(a ^ b for a, b in zip(key, ipad)) msg).digest() outer hashlib.sha256(bytes(a ^ b for a, b in zip(key, opad)) inner).digest() return outer key bmy-secret-key msg bhello world print(hmac_sha256(key, msg).hex())运行这串代码再把输出和标准库的结果对比import hmac print(hmac.new(key, msg, hashlib.sha256).hexdigest())两个结果完全一致。这说明标准库的实现本质就是把 RFC 2104 的流程封装好了。理解这个底层逻辑之后遇到为什么密钥太长要先哈希为什么填充用 0x36 和 0x5C这类问题看一眼代码就全明白了。至于为什么选这两个特定的字节值这是 RFC 设计时特意挑选的它们在二进制上具有很好的混淆特性能保证内外两次哈希的输入差异足够大。3. HMAC-SHA 家族选型的依据SHA-1、SHA-256 与 SHA-512 的取舍HMAC 本身是一个框架底层可以套用不同的哈希函数。市面上常见的组合有 HMAC-MD5、HMAC-SHA1、HMAC-SHA256、HMAC-SHA512 等等。命名里的HMAC-SHA通常泛指 HMAC 与 SHA 系列哈希函数的组合其中 HMAC-SHA256 是当前技术社区最主流的默认选择。3.1 各算法输出长度与安全强度对照选择哪种组合本质上是在安全强度、计算性能和兼容性之间做平衡。我整理了常用选项的对照表算法底层哈希输出长度分组长度安全强度建议场景HMAC-MD5MD516 字节64 字节弱已不推荐遗留系统兼容HMAC-SHA1SHA-120 字节64 字节中存在理论风险旧版本协议兼容HMAC-SHA256SHA-25632 字节64 字节强API 签名、Webhook、Token 生成HMAC-SHA512SHA-51264 字节128 字节强对安全要求极高的场景HMAC-SHA3-256SHA3-25632 字节144 字节强新系统评估后可采用SHA-1 的碰撞攻击在学术界已经被实际验证虽然 HMAC-SHA1 的构造方式使攻击难度大大增加但新系统已经没有理由继续用 SHA-1 了。MD5 更不用说碰撞成本极低只能用于低安全要求的兼容场景。HMAC-SHA256 输出 32 字节64 位十六进制字符串安全性、性能、兼容性都非常均衡绝大多数 API 签名场景选它都不会错。3.2 一个反直觉的点SHA-512 在 64 位系统上可能比 SHA-256 更快选型时有个有意思的现象在 64 位处理器上SHA-512 的运算速度有时反而比 SHA-256 快。原因在于 SHA-512 使用 64 位字长做运算而 SHA-256 使用 32 位字长。现代 CPU 对 64 位运算的硬件支持更好SHA-512 每一轮能处理更多的数据。当然实际快慢取决于具体实现和 CPU 架构不能一概而论。不过 SHA-512 的输出是 64 字节在某些场景下会带来额外开销。比如数据库字段存储、网络传输的签名头都会多占一些空间。一个折中方案是使用 SHA-512/256这是 SHA-512 系列的一个变种内部按 512 位运算但输出截断为 256 位既有 SHA-512 的性能优势又保持 32 字节的紧凑输出。只是这个算法在部分语言的标准库里支持不够普遍实际项目中用得相对少。3.3 密钥长度到底设多少合适HMAC 的密钥长度直接影响安全强度但并非越长越好。分组长度是上限密钥超过分组长度时会先被哈希压缩实际上参与运算的还是压缩后的值。因此密钥的有效长度不会超过底层哈希的输出长度。从强度匹配的角度看HMAC-SHA256 的密钥建议至少 32 字节256 位这是安全强度的合理上限。日常项目中常见的做法是使用一段 32 字节以上的随机字符串比如生成一个 64 位的十六进制随机串作为密钥。需要注意的是密钥的随机性比长度更重要使用123456这种弱密钥就算算法本身再安全也无济于事。生产环境的密钥应该通过密码学安全的随机数生成器产生并存放在配置中心或密钥管理服务中不要硬编码在代码里。4. 动手生成一个 HMAC代码、命令行与在线工具三路对比理解原理之后最关键的问题来了实际工作中到底怎么生成 HMAC-SHA 值我平时依赖三个途径编程语言内置库、操作系统的命令行工具、在线网页工具。三种方式各有适用场景我逐个展开说明。4.1 Python 与 Node.js 的一分钟实现绝大多数后端场景用 Python 或 Node.js 就能直接搞定。Python 标准库hmac模块的用法非常简单这里还是用最常见的 HMAC-SHA256 举例import hmac import hashlib key bplease-replace-with-a-random-secret message bGET\n/api/v1/orders\n2025-01-01T00:00:0008:00 signature hmac.new(key, message, hashlib.sha256).hexdigest() print(signature)Node.js 的实现同样简洁const crypto require(crypto); const key please-replace-with-a-random-secret; const message GET\n/api/v1/orders\n2025-01-01T00:00:0008:00; const signature crypto.createHmac(sha256, key) .update(message) .digest(hex); console.log(signature);这里有两个容易踩的坑。第一个是消息的编码问题message字符串在传输和计算过程中必须使用统一的编码。比如调用方用 UTF-8 编码计算签名接收方如果误用 ISO-8859-1 解码后再计算得到的签名肯定不一致。第二个是换行符问题如果签名原文里包含换行而 JSON 序列化时自动转义成了\n字面量两边算出来的签名就会对不上。这类问题我在联调中遇到过不止一次后面会专门讲。4.2 OpenSSL 命令行不需要写代码的验证利器有时候只是想快速验证一个签名对不对不想写任何代码OpenSSL 命令是最快的路子echo -n GET\n/api/v1/orders\n2025-01-01T00:00:0008:00 | openssl dgst -sha256 -hmac please-replace-with-a-random-secret -hex需要注意echo -n选项目的是不输出末尾的换行符。如果少写了-n消息内容会变成一个隐藏的换行符最终签名跟程序里计算的结果完全不同。这个坑在 shell 脚本自动化签名时尤其常见。OpenSSL 支持多种哈希算法把-sha256换成-sha1或者-sha512就能生成 HMAC-SHA1 或 HMAC-SHA512 的签名。命令行方式非常适合在开发环境做快速比对也适合集成到 CI/CD 脚本里对文件做完整性校验。4.3 在线工具的使用场景临时验证、跨语言比对在线工具主打一个零成本打开网页就能用。它的价值主要体现在两个场景一是联调时快速验证自己的计算结果是否正确二是跨语言调试时用同一个输入在工具和程序里分别计算看两边结果是否一致以此确认某个语言库的 HMAC 实现是否有问题。在线工具的典型界面一般有几个输入项密钥Secret Key、消息内容Message、选择的哈希算法SHA256/SHA512 等输出十六进制或 Base64 编码的签名串。用起来确实方便但这里面的门道不少。我见过不少工具表面上是个 HMAC 计算器实际上输入框里的密钥被你敲进去的瞬间已经通过网络请求传到了它的服务器。密钥这种东西等于把自家系统的钥匙交给了别人。所以在线工具的使用必须讲究方法具体怎么选、怎么用下一节展开讲。5. 在线生成工具的真正风险与安全使用姿势标题里既然提到在线生成 HMAC 的实用工具这一节就得把工具怎么选、怎么用说透。实际工作中完全不用在线工具也不现实毕竟不是每台电脑都配好了开发环境。但如果不知道工具的筛选标准很容易把生产环境的密钥泄露给第三方。5.1 最核心的筛选标准密钥是否只在本地计算用在线工具之前先问自己一个问题密钥能交给第三方网站吗答案显然是不能。那么问题就变成了如何判断一个在线工具到底是在本地算还是把数据偷偷传到了服务器一个简单有效的验证方法打开浏览器的开发者工具F12切到 Network网络面板然后在网页里输入密钥和消息并点击生成。如果 Network 面板里没有任何请求发出说明这个工具是纯前端 JavaScript 实现的密钥和数据都在浏览器本地完成计算没有出过你的电脑。如果看到有 HTTP 请求发到某个服务器地址尤其是把明文密钥作为请求参数传出去的这种工具立刻放弃。实践中有个更极端的测试办法直接把电脑网络断开飞行模式刷新页面后再点生成。如果计算结果仍然正常出现说明所有计算逻辑确实是浏览器本地执行的。断网后页面直接报错或者转圈那就说明它需要服务器帮忙密钥已经不安全了。5.2 为什么我最终选择自己做一个 HTML 本地工具经过几轮筛选我见过的很多在线HMAC 工具并不让人放心。有的工具确实纯前端实现但页面里藏着统计脚本、广告追踪脚本这些脚本虽然不直接窃取密钥但谁知道哪天被第三方注入了恶意代码呢。保守起见我最终的做法是用开源代码自己做一个 HTML 工具直接双击用浏览器打开本地文件实现完全离线计算。做法很简单用 Python 标准库的 hmac 模块先算好一组标准答案然后写一个单文件 HTML引入官方 CDN 上的 crypto-js 或者 js-sha256 库把在线工具暴露出来的几个功能复刻一遍。关键点是这个 HTML 文件是file://协议打开的本地文件不依赖任何远程资源断网状态下照常计算从根源上杜绝了数据外泄的可能。这种自建工具的好处是你完全知道它在干什么没有任何隐藏的网络请求和第三方脚本。一个最小可用的实现长这样用浏览器原生的 Web Crypto API连外部库都不用引!DOCTYPE html html head meta charsetutf-8 titleHMAC-SHA256 Local Tool/title /head body textarea idmsg placeholder消息内容/textareabr input idkey placeholder密钥br button onclickcalc()生成 HMAC-SHA256/button p idout/p script async function calc() { const enc new TextEncoder(); const keyData enc.encode(document.getElementById(key).value); const msgData enc.encode(document.getElementById(msg).value); const cryptoKey await crypto.subtle.importKey( raw, keyData, { name: HMAC, hash: SHA-256 }, false, [sign] ); const sig await crypto.subtle.sign(HMAC, cryptoKey, msgData); document.getElementById(out).textContent [...new Uint8Array(sig)] .map(b b.toString(16).padStart(2, 0)).join(); } /script /body /html把这段代码保存为.html文件双击用浏览器打开就能用。它完全离线运行密钥不会离开你的电脑。这个方案我现在一直保留在本地联调时随手打开算一个签名比任何在线工具都安心。5.3 就算工具可靠这些数据也不适合交出去即便找到了一个纯本地计算的在线工具也要注意使用场景。生产环境的密钥、核心业务的数据无论如何都不应该依赖在线工具处理。我给自己定了一条使用红线在线工具只用于非敏感数据的测试和验证比如联调环境里的模拟密钥、公开的样例数据。生产环境的密钥生成与签名计算一律走代码或命令行。还要警惕一类披着计算器外衣的钓鱼页面。有些域名跟正经工具非常相似但页面里嵌入了恶意脚本专门收集访问者的输入内容。如果你在一个可疑的网页里输入了密钥风险显然不只是签名串被算出来那么简单。遇到这类情况最稳妥的做法还是回到自己可控的工具链上。6. 我在落地 HMAC 签名时踩过的几个坑原理清楚了工具也选好了但真正上线的时候才会发现自己踩过的坑比想象的要多。这里分享几个我在实际项目中反复遇到的问题希望能帮读者省掉一些调试时间。6.1 编码不一致同一个字符串算出了不同的签名第一次做支付回调签名验证时我遇到了一个诡异的问题。支付平台给的签名示例能通过但换了自己的商户订单号就一直报签名错误。排查到最后发现平台的示例代码用的是 ASCII 编码读取参数字符串而我这边用 UTF-8 编码。参数里只要包含中文或特殊符号两种编码方式得出的字节序列完全不同签名自然对不上。解决方式是在协议层面约定好编码标准参数序列化、签名计算、Base64 编码全部统一使用 UTF-8。设计接口文档时这条约定要写明因为双方使用的开发语言可能不同默认编码也不一样。Node.js 和 Python 3 默认都是 UTF-8但 Java 的String.getBytes()如果不指定编码会依赖系统默认编码部署在不同操作系统上可能结果不同。6.2 签名串的构造规则拼接顺序比想象中敏感很多接口的签名规则是将所有请求参数按字典序排列用keyvalue的形式拼接再在首尾加上密钥。这里的细节很多比如字典序是按 ASCII 排序还是按 Unicode 排序、参数值是否做 URL 解码、空值参数要不要参与签名。不同的规则设计出来签名结果完全不同。我见过一个项目双方对接文档里只写了按参数名排序拼接没有规定具体的排序算法。结果甲方用 Java 的TreeMap字典序乙方用 Python 的sorted()按 Unicode 码点排序两边遇到大写字母和特殊字符时排序结果不一致前后排查了两个小时。后来我们约定所有通讯参数在签名前必须经过规范化处理包括排序规则、编码、拼接格式全部在接口文档里明确写出示例并且附带至少三组测试用例。6.3 密钥管理别把钥匙藏在代码里一个常见的反面教材是把 HMAC 密钥直接写在代码里然后提交到 Git 仓库。密钥一旦进了代码仓库就相当于永久泄露了因为提交历史里永远留有一份。更可怕的是自动化扫描工具比如 GitLeaks会非常敏感地抓取这类密钥外部攻击者也早已盯上公开仓库。比较稳妥的做法是把密钥放在环境变量、配置中心或专门的密钥管理系统里按环境区分开发环境、测试环境、生产环境各用不同的密钥。一旦发现密钥可能泄露立即在服务端更换密钥并通知所有调用方更新。换密钥的时候要考虑兼容期通常新老密钥并行一段时间等所有调用方都完成切换后再彻底废弃旧密钥。6.4 大小写、填充、截断输出格式的隐藏陷阱HMAC 计算得到的是二进制摘要展示和传输时通常编码为十六进制字符串或 Base64。这两种编码各有注意点十六进制字符串有大写和小写两种形式签名校验必须明确区分或做统一转换Base64 编码可能因为换行符或者末尾填充字符的处理不同而产生差异。有一个真实案例两个服务间做回调通知签名头字段用的是 Base64 编码但发送方在 JSON 序列化时对 Base64 字符串里的和/做了 URL 编码接收方拿到后没有先做 URL 解码就直接验签结果签名一直失败。这类格式转换的问题本质上还是两端对同一份数据的表达方式不一致造成的解决办法是在协议文档里给出一套完整的输入示例 输出示例用具体数据固化规则。6.5 防重放签名验证通过不代表请求安全最后提醒一个最容易忽略的点HMAC 只验证数据和来源的真实性不验证时间的有效性。攻击者拿到一个合法的签名请求后可以在任意时间重新发送服务端只要签名一致就会照常处理。之前提到的内部数据同步服务被篡改的案例解决之后我们很快又遇到了重复请求处理了两次的问题。后来我在签名规则里强制加了一个时间戳参数服务端验签通过后再检查当前时间与时间戳的差值是否在允许范围内通常 5 分钟到 15 分钟。对于更敏感的接口还可以再用随机数nonce机制将本次请求的随机数缓存起来同一随机数重复出现则拒绝。时间戳加 nonce 的组合是实际项目里对付重放攻击最常用的手段。HMAC-SHA 消息认证的价值简而言之就是一把密钥、一次双重哈希在不改变原有数据结构的前提下让接收方有办法验证数据没被改过、发送方是本人。它不复杂但细节决定成败。编码统一、密钥保管、签名规则、防重放机制每一环都值得在实际项目中认真对待。在线工具可以提升平时的开发调试效率但密钥和数据安全的底线必须守住。如果这套流程让你少走几次弯路那就值了。
阅读完成 · 觉得有帮助?
咨询建站