哈希算法:MD5和SHA家族
在web应用中,我们经常和哈希算法打交道。比如,将用户的密码进行 hash
处理后存入数据库、生成和校验下载文件的 MD5 校验和
等等。本篇聊聊在Java中,如何解决常见的哈希算法问题。
Hash算法及其特点
Hash(哈希),又称“散列”,基本上,在任何一门编程语言中,都会涉及到哈希函数。它将任意长度
的输入通过Hash算法变换成固定长度
的输出(散列值),也就是说,无论输入长度有多长,最终都会生成固定长度的输出,所以,这种转换是一种压缩
映射。另外,数据一旦被Hash处理后,无法通过逆向处理来得到原始输入值,这样,就保证了原始数据的安全性。
总结一下,其主要特点是:
- 确定性:对于同一个输入,无论在什么时候、什么地方、用什么设备计算,得到的输出都是相同的。
- 不可逆性:给定一个输出,无法推算出输入,只能通过穷举法来尝试找到输入。
- 敏感性:如果输入稍微改变了一点点,那么经过哈希算法,得到的输出将会变得面目全非。
- 碰撞抵抗性:很难找到两个不同的输入,使得它们经过哈希算法得到相同的输出。
MD5和SHA家族
Hash是一种思想,其具体的算法实现中,常见的有MD5和SHA家族(SHA-1,SHA-256,SHA-512等),MD5算法在早期非常流行,但是,现在已经被证明为其不安全
,在安全性有要求的场景下,不要使用该算法,它的输出由四个32位分组
组成,通过将这四个分组组合后,最终生成一个128位
散列值。
SHA是Secure Hash Algorithm
的简称,分别是SHA-1、SHA-224、SHA-256、SHA-384,和SHA-512,后四者有时并称为SHA-2,SHA-1已经被证实不安全
,所以,现在一般较少使用,使用较多的是SHA-256等SHA-2。
哈希算法具有广泛的应用,下面来举三个例子来说明。
应用一:密码hash处理
当用户注册时,拿到用户传递过来的密码后,一般不会将这个密码直接存入数据库中,因为明文的密码非常敏感,一旦数据库泄露,那么,用户的密码就会被直接看到,所以,在存入数据库前,会对用户的密码进行hash处理,如下:
String password = "abc";
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hashBytes = digest.digest(password.getBytes());
// 将字节数组转换为十六进制字符串
StringBuilder sb = new StringBuilder();
for (byte b : hashBytes) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
System.out.println(sb); // 存入数据库中
} catch (NoSuchAlgorithmException e) {
System.err.println("SHA-256 algorithm not found!");
}
上面代码中,首先使用 MessageDigest.getInstance("SHA-256")
得到 digest 对象。接着,调用 digest()
方法得到签名的字节数组。最后,将值转换为十六进制格式。我们使用的是sha256算法,它会生成256bit的散列值,如果用十六进制字符串表示,长度是64,因此,上面我们得到的字符串长度为64。
请注意,上面的处理方式有一个缺陷:它使用固定的 secret 进行计算,容易被撞库解密。为了提高安全性,我们可以选择更安全的HmacSHA256
,它可接受一个自定义 secret:
public static void main(String[] args) {
String secret = "your_secret";
String password = "abc";
try {
Mac mac = Mac.getInstance("HmacSHA256");
var secretKeySpec = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
mac.init(secretKeySpec);
// 计算HMAC-SHA256
byte[] hash = mac.doFinal(password.getBytes());
// 将结果转换为十六进制字符串
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
System.out.println(hexString);
} catch (Exception e) {
e.printStackTrace();
}
}
除非 secret 被泄露,否则,基本不可能通过撞库来破解,极大地加强了应用的安全性。
应用二:生成MD5校验和
在从某些网站下载文件时,网站会提供一个MD5校验值或sha256值。比如,从ubuntu中国镜像站(上海交通大学)中下载iso文件时,还提供了一个MD5SUMS文件,其内容如下:
里面显示了每个下载文件的MD5值,当我们下载好文件后,通过下载后的文件在本地生成MD5值(用第三方工具),或使用如下代码来生成:
public String createMd5() throws IOException, NoSuchAlgorithmException {
var path = Paths.get("resources/wubi.exe");
byte[] fileData = Files.readAllBytes(path);
var md5 = MessageDigest.getInstance("MD5");
byte[] md5Bytes = md5.digest(fileData);
// 转换为16进制字符串
var sb = new StringBuilder();
for (byte b : md5Bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
得到的值为:b31731ea6cdbebe1d02f8193db420886。对比发现,我们生成的MD5值和网站上提供的MD5值是一样的,则可以确保这个wubi.exe文件的完整性,没有被篡改,可以安全使用。
注意:正如上面所说,MD5并不安全,所以,最好校验其sha256值,而不是MD5值。
应用三:垃圾邮件过滤
在电子邮件中,如何对垃圾邮件进行过滤呢?哈希算法也可以派上用场,思路如下:
- 建立一个邮件 MD5 值资料库,分别储存邮件的 MD5 值、允许出现的次数(假定为 3)和出现次数(初值为零)。
- 对每一封收到的邮件,将它的正文部分进行MD5 计算,得到 MD5 值,将这个值在资料库中进行搜索。
- 如未发现相同的 MD5 值,说明此邮件是第一次收到,将此 MD5 值存入资料库,并将出现次数置为1,转到第五步。
- 如发现相同的 MD5 值,说明收到过同样内容的邮件,将出现次数加 1,并与允许出现次数相比较,如小于允许出现次数,就转到第五步。否则中止接收该邮件,结束。
- 接收该邮件。