第二章 哈希算法与消息摘要:MD5、SHA家族、HMAC在接口校验中的应用

说到接口加密,我第一个想到的其实不是AES或者RSA这些复杂的算法,而是哈希。

为什么?因为我们做接口测试,最常遇到的一个场景就是验签。服务端给你返回一串数据,外加一个签名。你得自己算一遍这个签名,看看跟人家给的对不对得上。这个签名用到的底层技术,就是哈希算法。

说白了,哈希就是一种“指纹”技术——你给它任意长度的数据,它给你算出一个固定长度的摘要。这个摘要,就可以代表原始数据。

2.1 消息摘要的基本概念

消息摘要,也叫数字摘要。它的核心特性就三个:

  • 固定长度输出:不管原文是1个字符还是1个G,算出来的摘要长度都一样。
  • 不可逆:从摘要你没法反推出原文。这其实也是哈希被称为“单向函数”的原因。
  • 抗碰撞:很难找到两个不同的输入,能得到同样的输出。

为什么接口测试要用它?

因为我们需要一种机制来验证“数据在传输过程中有没有被篡改”。哈希正好干这个。你发送数据时算一次摘要,别人收到数据时再算一次。两个摘要一比对,就知道数据是不是原装的。

我在项目中见过很多新人,上来就用哈希算法去加密敏感字段,比如密码。这其实是误用。哈希不是加密,它不负责保护数据机密性,只负责数据完整性。密码存数据库用的是哈希的不可逆特性,那是另外一个故事了。

特性 含义 接口测试中的价值
固定长度 输出长度不依赖输入 签名域段长度可预测,便于协议解析
不可逆 无法从摘要还原原文 签名本身不泄露敏感信息
抗碰撞 不同输入几乎不会产生同样输出 唯一标识数据,防止抵赖
高效计算 硬件级别优化,性能远优于非对称加密 高并发场景下不影响接口响应时间

2.2 MD5:经典但已淘汰

MD5,全称Message Digest Algorithm 5。它曾是应用最广的哈希算法,现在已经不推荐用在安全场景了。

我职业生涯早期,很多接口签名用的都是MD5。后来有一次做安全测试,我拿一个彩虹表去撞库,发现5分钟内就能找到几个碰撞实例。那之后,我在公司内部推行了一个规范:所有新接口签名,严禁使用裸MD5。

警告:MD5已经能被轻易碰撞。2004年,中国山东大学的王小云教授团队就公布了MD5的碰撞攻击方法。2017年,谷歌更是公开了SHA-1的两个碰撞实例。MD5的情况更糟。

但也不是完全不能用。我个人的习惯是:MD5只用于非安全场景的校验,比如数据分片后的快速比对、消息去重、或者对日志内容做防重复索引。

2.3 SHA家族:现在的主力

SHA(Secure Hash Algorithm)是美国国家标准与技术研究院发布的一系列哈希算法。目前市面上主要是SHA-1和SHA-2家族。

  • SHA-1:输出160位(20字节)。跟MD5一样,已经被找到碰撞攻击方法。不建议在新项目中使用。
  • SHA-256 / SHA-384 / SHA-512:属于SHA-2家族。输出长度不同,安全性逐级递增。目前是业界标准。

为什么SHA-256比MD5安全?说白了,就是位长更长、轮数更多、设计更复杂。但我建议你直接记住结论:SHA-256可以满足当前几乎所有接口验签场景。如果上游厂商要求用SHA-256,你不用犹豫,直接用。

算法 输出位数 安全性等级 适用场景
MD5 128 bit 已攻破 非安全校验、去重、分片
SHA-1 160 bit 已攻破 不推荐
SHA-256 256 bit 安全 接口签名、证书、完整性校验
SHA-512 512 bit 极高 高安全级别或合规要求场景

2.4 HMAC:带密钥的哈希

你可能会问:既然哈希这么强大,我的接口签名直接用SHA-256(消息体)不就行了吗?

不够。如果你直接用算法名+消息体,那任何人只要知道你的算法,就能算出跟你一模一样的签名。没有密钥保护,签名就失去了意义。

所以有了HMAC(Hash-based Message Authentication Code)。它把密钥跟消息搅在一起,再通过哈希算法计算,输出一个基于密钥的消息认证码。

你在接口文档里经常看到这样的签名公式:sign = HmacSHA256(secret, requestBody)。这,才是真正实用的签名方式。

我想强调的实战细节:

HMAC的计算顺序和格式,不同厂商定义不同。我遇到过最坑的一次,某厂商要求先拼接key=value&key=value字符串,再对这个字符串做HmacSHA256。但另一家却要求先对value做一次MD5,再做HMAC。所以不要只看算法名称,一定要看清楚“待签名的内容到底是什么”。

来看一个HmacSHA256的代码示例,接口测试中经常用到:

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class HmacUtil {
    public static String hmacSha256(String data, String key) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        SecretKeySpec secretKey = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256");
        mac.init(secretKey);
        byte[] rawHmac = mac.doFinal(data.getBytes("UTF-8"));
        return Base64.getEncoder().encodeToString(rawHmac);
    }
}

如果是Python,更简洁:

import hmac
import hashlib
import base64

def hmac_sha256(data: str, key: str) -> str:
    sig = hmac.new(key.encode(), data.encode(), hashlib.sha256).digest()
    return base64.b64encode(sig).decode()

2.5 接口校验中的实战经验

在接口测试时,哈希的使用场景主要有三个:

  1. 请求签名:客户端用HMAC+密钥,对请求参数或请求体做签名,放在header里。服务端收到后用同样的密钥和签名逻辑验一次。
  2. 响应完整性:服务端返回数据时,附带签名字段。客户端拿到后计算签名,确认响应没被篡改。
  3. 回调通知签名:第三方支付/撮合平台异步通知时,参数都拼成字符串再做签名验签。这个坑特别多,字段顺序、空值处理、编码都得对齐。

避坑提醒:我曾经在测试一个支付回调接口时,发现对方的文档里写着“对所有参数按key升序排列,拼接成key1=value1&key2=value2,再做MD5”。

结果实际跑通后,验签一直失败。最后排查发现,他们的MD5是对小写字符串计算的,而我试了默认大小写。细节决定成败,在接口签名测试里,大小写、编码(UTF-8/GBK)、空值处理、排序顺序,每一个都是潜在的问题点。

2.6 哈希与MAC的区别:一张图看懂

很多人搞不清哈希和MAC的区别。一句话:哈希是算法,MAC是认证机制。MAC通过哈希算法加密钥实现,所以叫HMAC。

消息摘要原理与核心逻辑 原始消息 密钥(Secret) HmacSHA256 消息认证码(MAC) 服务端计算出签名 (同样算法+密钥) 客户端接收到签名 (接口返回或回调携带) 匹配 = 校验通过 | 不匹配 = 数据已被篡改

这个图的逻辑很简单:原始消息加上密钥,一起进HmacSHA256算法,算出MAC。服务端和客户端各自算一遍,比对结果。不匹配,就说明消息在传输过程中被动过手脚。

我总结的三个关键点:

  • 密钥共识:双方事先约定好密钥,密钥必须保密。
  • 摘要长度:HmacSHA256输出32字节,相比MD5的16字节,安全余量大很多。
  • 时序一致:验签时,被签名的内容(比如时间戳+请求体+随机数)必须跟签名时完全一致。我在项目中见到的最常见错误就是时间戳精度不一致,导致验签失败。

2.7 哈希与加密的区别(测试人员必须分清)

很多测试同学问我:哈希是不是加密的一种?

不是,完全两回事。

维度 哈希(Hash) 加密(Encryption)
是否可逆 不可逆 可逆(通过解密)
输出长度 固定 通常与输入长度相关
是否需要密钥 不需要(裸哈希)或需要(HMAC) 必须
目的 完整性校验、身份认证 机密性保护
接口测试场景 签名验签、密码验证 传输敏感数据、字段脱敏

一个实用的测试技巧:

我习惯在自动化测试框架里加一个“签名工具类”,把常见的HMAC、SHA-256、MD5都封装到一起。这样无论是做正向验签,还是模拟篡改数据看服务端反应,都能一键调用。你可以把这个工具类的代码放到公共代码库,团队所有人都能用,避免重复造轮子。

嗯,这一章的内容就到这里。哈希算法听起来简单,但实际用起来坑不少。记住我上面讲的几个避坑点:密钥保护、签名内容一致性、算法选择(能用SHA-256别用MD5,能用HMAC别用裸哈希)。