BCrypt 密码加密:Spring Security 工具类
使用 Spring Security BCryptPasswordEncoder 生成和验证密码哈希的实用工具。
BCrypt 密码加密:为什么不能用 MD5
背景
在 CheckByAI 项目中,我需要在数据库中存储用户密码。初学时最自然的想法是用 MD5 哈希——一句 DigestUtils.md5DigestAsHex() 就搞定了。但 MD5 设计目标就是快——GPU 每秒可以计算数十亿次 MD5,暴力破解成本极低。
MD5 为什么不适合存密码
// 这样存储是不安全的
String hashed = DigestUtils.md5DigestAsHex(password.getBytes());
两个致命问题:
- 速度快:现代 GPU 每秒可算 200 亿次 MD5,8 位纯数字密码 0.1 秒破解
- 无盐值:同样的密码生成同样的哈希,彩虹表可以逆向查询
加盐(salt)可以解决彩虹表问题,但盐值需要单独存储和管理,自己实现的盐值管理容易出错。
BCrypt 的设计原理
BCrypt 专门为密码存储设计,核心设计理念是慢:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // strength 默认 10,即 2^10 轮
}
工作流程
- 自动生成随机盐值(22 个字符,嵌入在结果字符串中)
- 执行 2^strength 轮 Blowfish 加密(默认 2^10 = 1024 轮)
- 输出格式:
$2a$10$[22位盐值][31位哈希值]
// 每次加密结果不同
encoder.encode("admin123")
// $2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
// $2a$10$dQZrRjGiqQjX5nLjKjKJk.vb5iSjZn4qGysKLzGXJSjK.d5a8X5uS
// 两次 encode 结果不同,但 matches() 验证都能通过
强度(strength)参数
// strength=4 最低 16 轮,测试用
// strength=10 默认 1024 轮,约 100ms
// strength=12 4096 轮,约 400ms
new BCryptPasswordEncoder(10)
strength 每 +1,破解时间翻倍。摩尔定律带来的算力提升可以被 strength 参数抵消——2024 年的 strength=10 和 2014 年的 strength=10 都是约 100ms。
生产实践
在 CheckByAI 项目中,用 BCrypt 替代了之前的 MD5:
@Service
public class AuthService {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserRepository userRepository;
public User register(String username, String password) {
User user = new User();
user.setUsername(username);
user.setPassword(passwordEncoder.encode(password)); // BCrypt 加密
return userRepository.save(user);
}
public boolean login(String username, String password) {
User user = userRepository.findByUsername(username);
return user != null && passwordEncoder.matches(password, user.getPassword());
}
}
Spring Security 的 PasswordEncoder 接口设计得很好——加密用 encode(),验证用 matches()。切换算法只需要换实现类,业务代码不受影响。
替代方案
如果 BCrypt 的性能还不能满足需求(比如高并发登录场景),可以考虑:
- SCrypt:内存密集型的慢哈希,ASIC 和 GPU 加速效果有限
- Argon2:BCrypt 的现代替代,2023 年密码哈希竞赛胜出者
- PBKDF2:如果项目必须用 JDK 内置算法(不需要额外依赖)
但在大部分场景下,BCrypt 已经足够好。不要为了微乎其微的性能差异牺牲安全性。