1. 概述

在 Spring Security 4 中,使用内存认证(in-memory authentication)时可以直接存储明文密码,非常方便。但这种做法显然存在安全隐患。

到了 Spring Security 5,密码管理机制迎来一次重大重构——默认要求必须使用密码编码器(PasswordEncoder)。这意味着如果你的应用还在用明文存密码,升级到 Spring Security 5 后会直接报错,认证流程走不通。

本文就来聊聊这个常见的“升级踩坑”问题,并给出简单粗暴的解决方案。


2. Spring Security 4 的配置方式

先看一个典型的 Spring Security 4 风格的内存认证配置:

@Configuration
public class InMemoryAuthWebSecurityConfigurer 
  extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) 
      throws Exception {
        auth.inMemoryAuthentication()
          .withUser("spring")
          .password("secret")
          .roles("USER");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
          .antMatchers("/private/**")
          .authenticated()
          .antMatchers("/public/**")
          .permitAll()
          .and()
          .httpBasic();
    }
}

这段代码做了两件事: ✅ 为 /private/** 路径启用认证 ✅ 允许 /public/** 路径匿名访问

看起来没啥问题,但在 Spring Security 5 下运行,会直接抛出异常:

java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"

⚠️ 错误原因很明确:没有配置 PasswordEncoder,框架不知道怎么解码你存的密码

Spring Security 5 要求所有密码都必须带上编码类型前缀(如 {bcrypt}),否则无法识别。


3. Spring Security 5 的正确姿势

3.1 使用 DelegatingPasswordEncoder

Spring Security 5 引入了 DelegatingPasswordEncoder,它能根据密码前缀自动选择对应的编码器。推荐通过 PasswordEncoderFactories 工具类创建:

@Configuration
public class InMemoryAuthWebSecurityConfigurer {

    @Bean
    public InMemoryUserDetailsManager userDetailsService() {
        PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
        UserDetails user = User.withUsername("spring")
            .password(encoder.encode("secret"))
            .roles("USER")
            .build();
        return new InMemoryUserDetailsManager(user);
    }
}

这样存储的密码长这样:

{bcrypt}$2a$10$MF7hYnWLeLT66gNccBgxaONZHbrSMjlUofkp50sSpBw2PJjUqU.zS

✅ 前缀 {bcrypt} 表明使用 BCrypt 编码
✅ 框架能自动识别并验证
✅ 符合 Spring Security 5 安全规范

📌 建议:除非有特殊需求,否则直接使用 PasswordEncoderFactories.createDelegatingPasswordEncoder() 提供的默认编码器集合即可,它已经内置了 bcrypt、scrypt、pbkdf2 等主流算法的支持。

⚠️ 注意:从 Spring Security 5.7.0-M2 开始,WebSecurityConfigurerAdapter 已被标记为 @Deprecated,官方建议使用基于组件的配置方式(如 SecurityFilterChain Bean)。详细迁移方案可参考 Spring 废弃 WebSecurityConfigurerAdapter 的说明


3.2 特殊情况:不想编码密码?用 {noop}

如果你只是做本地测试,或者暂时不想处理密码编码,可以使用 {noop} 前缀,告诉框架“别编码,原样比对”:

@Configuration
public class InMemoryNoOpAuthWebSecurityConfigurer {

    @Bean
    public InMemoryUserDetailsManager userDetailsService() {
        UserDetails user = User.withUsername("spring")
            .password("{noop}secret")
            .roles("USER")
            .build();
        return new InMemoryUserDetailsManager(user);
    }
}

✅ 实现简单,适合开发/测试环境
绝对不要在生产环境使用!

📌 官方早已将 NoOpPasswordEncoder 标记为过时(deprecated),因为它本质上就是明文存储,存在严重安全风险。


3.3 已有密码如何迁移?

如果你正在从旧版本升级,数据库里存的可能是明文或老式哈希密码,可以按以下策略迁移:

✅ 方案一:明文密码 → 编码后存储

对现有明文密码进行 BCrypt 编码:

String encoded = new BCryptPasswordEncoder().encode(plainTextPassword);
// 存入数据库:{bcrypt}$2a$10$...

✅ 方案二:已哈希密码 → 添加编码前缀

如果原来用的是 SHA-256 等算法,只需加上对应前缀即可:

{bcrypt}$2a$10$MF7hYnWLeLT66gNccBgxaONZHbrSMjlUofkp50sSpBw2PJjUqU.zS
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0

Spring Security 会根据前缀自动调用对应的 PasswordEncoder

✅ 方案三:未知编码方式 → 引导用户重置密码

如果老系统密码编码方式不明确(比如用了自定义算法),最稳妥的方式是:

  1. 登录时检测密码是否带前缀
  2. 若无前缀,提示用户“首次登录需重置密码”
  3. 重置时用新编码器存储

这样既能平滑过渡,又能逐步提升系统安全性。


4. 总结

Spring Security 5 对密码安全的强化是件好事,虽然给升级带来了一点小麻烦,但只要理解了 DelegatingPasswordEncoder 的工作原理,处理起来并不复杂。

关键点回顾:

  • ❌ 不再支持无编码的明文密码
  • ✅ 推荐使用 PasswordEncoderFactories.createDelegatingPasswordEncoder()
  • ✅ 密码需带前缀,如 {bcrypt}{noop}
  • ⚠️ 生产环境禁用 {noop}
  • 🔁 老系统升级时注意密码迁移策略

源码示例已上传至 GitHub:https://github.com/baeldung/spring-security-tutorial(模块:spring-security-web-rest-basic-auth)


原始标题:Default Password Encoder in Spring Security 5