在使用 Spring Security 构建用户认证系统时,我们通常会通过 BCryptPasswordEncoder 对用户密码进行加密存储,以保障安全性。但在实现“修改密码”功能时,我曾犯过一个看似低级却非常典型的错误——对用户输入的旧密码再次加密,然后与数据库中的密文直接比对

直到调试时才发现问题所在。今天就来分享这个踩坑经历,并说明为什么必须使用 PasswordEncoder.matches() 方法来验证密码

🚫 错误做法:对旧密码重新加密后比对

最初,我的“修改密码”逻辑是这样写的:

// ❌ 错误示例:不要这样做!
String oldPassword = updatePassWordDto.getOldPassword();
String reEncoded = passwordEncoder.encode(oldPassword);

if (!reEncoded.equals(sysUser.getPassword())) {
    throw new BaseException("旧密码错误");
}

乍一看似乎合理:用户输入旧密码 → 加密 → 和数据库里存的加密密码比较。

但问题在于:BCrypt 是一个加盐(salted)的单向哈希算法,每次加密结果都不同!

即使输入完全相同的明文密码,两次调用encode()也会生成两个完全不同的密文。例如:

BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
System.out.println(encoder.encode("123456")); // $2a$10$X1...A1
System.out.println(encoder.encode("123456")); // $2a$10$Y2...B2 (完全不同!)

因此,reEncoded.equals(sysUser.getPassword()) 永远返回 false,导致用户无论如何都无法通过旧密码校验。

💡 我就是在本地调试时发现:明明输对了旧密码,系统却一直报错。断点一打,才发现两次加密结果根本对不上!

✅ 正确做法:使用 matches() 方法

boolean matches(CharSequence rawPassword, String encodedPassword);

正确的“修改密码”逻辑应如下:

// ✅ 正确示例
if (!passwordEncoder.matches(updatePassWordDto.getOldPassword(), sysUser.getPassword())) {
    throw new BaseException("旧密码错误");
}

🔍 matches() 是如何工作的?

  1. 数据库存储的 BCrypt 密文(如 $2a$10$abc...xyz)内部已包含随机 salt 和 hash 值;

  2. matches() 会:

  • 自动从密文中提取 salt;

  • 使用该 salt 对用户输入的明文密码进行哈希;

  • 比较新生成的 hash 与密文中的 hash 是否一致。

  • 整个过程由 BCrypt 算法自动完成,无需手动处理 salt 或加密逻辑

📦 配置参考

确保你的 Spring Security 配置中已声明 PasswordEncoder Bean:

@Configuration
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

✅ 总结

  • 永远不要对用户输入的密码调用 encode() 后与数据库比对;

  • 验证密码的唯一正确方式是使用 passwordEncoder.matches(明文, 密文)

  • BCrypt 的安全性正来自于其“每次加密结果不同”的特性,而 matches() 方法正是为此设计的配套工具。

安全无小事,细节定成败。希望这篇文章能帮你避开这个“看似合理实则致命”的陷阱!