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
。
✅ 方案三:未知编码方式 → 引导用户重置密码
如果老系统密码编码方式不明确(比如用了自定义算法),最稳妥的方式是:
- 登录时检测密码是否带前缀
- 若无前缀,提示用户“首次登录需重置密码”
- 重置时用新编码器存储
这样既能平滑过渡,又能逐步提升系统安全性。
4. 总结
Spring Security 5 对密码安全的强化是件好事,虽然给升级带来了一点小麻烦,但只要理解了 DelegatingPasswordEncoder
的工作原理,处理起来并不复杂。
关键点回顾:
- ❌ 不再支持无编码的明文密码
- ✅ 推荐使用
PasswordEncoderFactories.createDelegatingPasswordEncoder()
- ✅ 密码需带前缀,如
{bcrypt}
、{noop}
- ⚠️ 生产环境禁用
{noop}
- 🔁 老系统升级时注意密码迁移策略
源码示例已上传至 GitHub:https://github.com/baeldung/spring-security-tutorial(模块:spring-security-web-rest-basic-auth)