哈希算法:MD5和SHA家族

阅读 1.7k
标签: Java

在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值。

应用三:垃圾邮件过滤

在电子邮件中,如何对垃圾邮件进行过滤呢?哈希算法也可以派上用场,思路如下:

  1. 建立一个邮件 MD5 值资料库,分别储存邮件的 MD5 值、允许出现的次数(假定为 3)和出现次数(初值为零)。
  2. 对每一封收到的邮件,将它的正文部分进行MD5 计算,得到 MD5 值,将这个值在资料库中进行搜索。
  3. 如未发现相同的 MD5 值,说明此邮件是第一次收到,将此 MD5 值存入资料库,并将出现次数置为1,转到第五步。
  4. 如发现相同的 MD5 值,说明收到过同样内容的邮件,将出现次数加 1,并与允许出现次数相比较,如小于允许出现次数,就转到第五步。否则中止接收该邮件,结束。
  5. 接收该邮件。

参考

最后编辑于: 2025-01-21

评论(0条)

(必填)
复制成功