1. 概述

本文将探讨密码哈希的重要性。

我们会快速了解一下什么是哈希、为什么它很重要,以及在 Java 中实现安全与不安全哈希的一些方式。

2. 什么是哈希?

哈希 是通过一种称为加密哈希函数的数学函数,从给定的输入(消息)生成一个固定长度的字符串(即哈希值)的过程。

虽然有很多哈希函数,但用于密码哈希的函数必须具备以下四个关键特性,才能被认为是安全的:

  1. 确定性:相同的消息使用相同的哈希函数应始终产生相同的哈希值
  2. 不可逆性:无法从哈希值反推出原始消息
  3. 高熵性:消息的小幅变化会导致哈希值的巨大差异
  4. 抗碰撞性:两个不同消息不应产生相同的哈希值

满足这四点的哈希函数是密码哈希的良好候选者,因为它们极大地增加了从哈希值反推密码的难度。

⚠️ 此外,密码哈希算法还应该是慢的。如果算法执行速度快,会助长暴力破解攻击(brute-force attack),攻击者每秒可以尝试数十亿甚至数万亿次猜测。

目前推荐使用的安全哈希算法包括:PBKDF2、BCrypt 和 SCrypt。不过,在介绍这些之前,我们先来看看一些过时且不再推荐的算法。

3. 不推荐使用:MD5

MD5 是 1992 年开发的消息摘要算法。

Java 提供的 MessageDigest 类可以轻松计算 MD5 值,在某些场景下仍有用武之地。

❌ 但近年来发现,MD5 已经无法满足抗碰撞性要求,生成碰撞变得相对容易。再加上 MD5 是一个快速算法,对暴力破解毫无抵抗力。

因此,MD5 不再推荐用于密码哈希

4. 不推荐使用:SHA-512

接下来我们看 SHA-512,它是 SHA 家族中的一员,最早可追溯到 1993 年的 SHA-0。

4.1. 为什么是 SHA-512?

随着计算机性能提升和新漏洞被发现,SHA 算法也在不断演进。SHA-512 是第三代算法中密钥最长的一个版本。

尽管现在已有更安全的 SHA 版本,但在 Java 标准库中,SHA-512 是目前支持的最安全版本

4.2. 在 Java 中实现 SHA-512

在实现 SHA-512 哈希前,我们先理解一下“盐”(salt)的概念。

📌 Salt 是为每个密码随机生成的一段数据,引入 salt 可以提高哈希的熵值,并防止彩虹表攻击。

最终的哈希过程大致如下:

salt <- generate-salt;
hash <- salt + ':' + sha512(salt + password)

4.3. 生成 Salt

我们使用 java.security 包中的 SecureRandom 类来生成 salt:

SecureRandom random = new SecureRandom();
byte[] salt = new byte[16];
random.nextBytes(salt);

接着使用 MessageDigest 配置 SHA-512 并加入 salt:

MessageDigest md = MessageDigest.getInstance("SHA-512");
md.update(salt);

然后调用 digest 方法生成哈希后的密码:

byte[] hashedPassword = md.digest(passwordToHash.getBytes(StandardCharsets.UTF_8));

4.4. 为什么不推荐?

虽然加了 salt 后 SHA-512 表现尚可,但相比 PBKDF2、BCrypt 和 SCrypt,它仍然 速度太快,而且缺乏可配置强度的功能。

5. 推荐算法:PBKDF2、BCrypt 和 SCrypt

这三个算法是目前业界推荐的密码哈希方案。

5.1. 为什么推荐?

它们都有两个优点:

  • ✅ 执行速度较慢(防暴力破解)
  • ✅ 支持配置强度参数(应对硬件进步)

这意味着随着计算能力提升,我们只需调整参数即可让算法变慢。

5.2. 实现 PBKDF2

同样需要 salt,这里也使用 SecureRandom

SecureRandom random = new SecureRandom();
byte[] salt = new byte[16];
random.nextBytes(salt);

然后创建 PBEKeySpecSecretKeyFactory,使用 PBKDF2WithHmacSHA1 算法:

KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 65536, 128);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");

其中第三个参数 65536 就是迭代次数,越大越慢,安全性越高。

最后生成哈希:

byte[] hash = factory.generateSecret(spec).getEncoded();

5.3. 实现 BCrypt 和 SCrypt

遗憾的是,Java 标准库中并不包含 BCrypt 和 SCrypt 的实现

不过我们可以借助第三方库,比如 Spring Security。

6. 使用 Spring Security 进行密码哈希

虽然 Java 原生支持 PBKDF2 和 SHA,但 BCrypt 和 SCrypt 需要额外依赖。

幸运的是,Spring Security 内置了这些算法的支持,通过 PasswordEncoder 接口统一管理:

  • Pbkdf2PasswordEncoder:用于 PBKDF2
  • BCryptPasswordEncoder:用于 BCrypt
  • SCryptPasswordEncoder:用于 SCrypt

这些编码器都支持配置哈希强度,而且 内部自动生成并管理 salt,无需手动处理。

你可以在非 Spring Security 项目中直接使用这些类;如果使用了 Spring Security,则可以通过 DSL 或依赖注入进行配置。

7. 总结

本文深入讲解了密码哈希的核心概念及其实现方式。

我们回顾了一些历史算法(如 MD5、SHA-512)为何不再适用,并介绍了当前推荐的安全算法(PBKDF2、BCrypt、SCrypt)。

最后,我们演示了如何借助 Spring Security 快速集成这些算法。

一如既往,代码示例可在 GitHub 上获取