1. 概述
本文将探讨密码哈希的重要性。
我们会快速了解一下什么是哈希、为什么它很重要,以及在 Java 中实现安全与不安全哈希的一些方式。
2. 什么是哈希?
哈希 是通过一种称为加密哈希函数的数学函数,从给定的输入(消息)生成一个固定长度的字符串(即哈希值)的过程。
虽然有很多哈希函数,但用于密码哈希的函数必须具备以下四个关键特性,才能被认为是安全的:
- ✅ 确定性:相同的消息使用相同的哈希函数应始终产生相同的哈希值
- ✅ 不可逆性:无法从哈希值反推出原始消息
- ✅ 高熵性:消息的小幅变化会导致哈希值的巨大差异
- ✅ 抗碰撞性:两个不同消息不应产生相同的哈希值
满足这四点的哈希函数是密码哈希的良好候选者,因为它们极大地增加了从哈希值反推密码的难度。
⚠️ 此外,密码哈希算法还应该是慢的。如果算法执行速度快,会助长暴力破解攻击(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);
然后创建 PBEKeySpec
和 SecretKeyFactory
,使用 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
:用于 PBKDF2BCryptPasswordEncoder
:用于 BCryptSCryptPasswordEncoder
:用于 SCrypt
这些编码器都支持配置哈希强度,而且 内部自动生成并管理 salt,无需手动处理。
你可以在非 Spring Security 项目中直接使用这些类;如果使用了 Spring Security,则可以通过 DSL 或依赖注入进行配置。
7. 总结
本文深入讲解了密码哈希的核心概念及其实现方式。
我们回顾了一些历史算法(如 MD5、SHA-512)为何不再适用,并介绍了当前推荐的安全算法(PBKDF2、BCrypt、SCrypt)。
最后,我们演示了如何借助 Spring Security 快速集成这些算法。
一如既往,代码示例可在 GitHub 上获取。