我在使用 Spring Security 时,自定义了 UserDetailsService
接口的实现类,需要实现 loadUserByUsername()
方法,把原来默认从内存中查询用户信息,修改成从 MySQL 数据库中查询用户名和密码。这样 Spring Security 就会自动使用我们自定义的 UserDetailsService
实现类进行用户的查询。将查询到的 User
对象封装成 UserDetails
的实现类 LoginUser
对象返回。
然后,我启动服务,在浏览器访问目标 URL 时,确实能正常被拦截在登录页面。正当我输入数据库 user
表里的用户名和明文密码后,点击【Sign in】后,
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
之所以会出现这种情况,是因为 Spring Security 会使用一个默认的 PasswordEncoder
,用作密码校验的编解码工具。而这个默认的 PasswordEncoder
要求 MySQL 数据库中密码字段值加上大括号 {}
前缀,大括号里面填写编码标识。
举个例子,如果 MySQL 数据库中密码字段值是明文 (即没有加密的) ,则需要在密码前面加上前缀 {noop}
,表示密码没有经过编码器加密,是明文存储的。
加上前缀 {noop}
后,可以不用重启项目,再次使用 MySQL 数据库中的用户名与密码就能成功登录了。
但以上方法有 2 点是与实际开发不符的,首先,我们不会直接以明文的方式存储密码,这样是非常不安全的,而是存储加密后的 “暗文” ;其次,我们也不会在每一条密码前都加前缀 {noop}
,这样太繁琐了。
真正实际开发的做法请看下面。
上面说过,用户密码以明文的方式存储在 MySQL 数据库中是十分不安全的。而 Spring Security 默认的 PasswordEncoder
要求 MySQL 数据库中密码字段值加上大括号 {}
前缀,大括号里面填写密码加密编码器标识。但是我们在实际开发中一般不会采用这种方式,因此就需要替换 PasswordEncoder
。
我们一般替换为 Spring Security 为我们提供的 BCryptPasswordEncoder
。BCrypt 是一款跨平台文件加密工具。
BCryptPasswordEncoder
使用起来也非常简单,只需要把 BCryptPasswordEncoder
对象注入 Spring 容器中,Spring Security 就会自动使用该密码编码器来进行密码校验。
我们可以定义一个 Spring Security 的配置类 SecurityConfig.java
,要求继承 WebSecurityConfigurerAdapter
。
config/SecurityConfig.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 创建BCryptPasswordEncoder注入Spring容器
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
BCryptPasswordEncoder
对象最核心的 2 个方法分别是:
方法 | 描述 |
---|---|
encode(CharSequence rawPassword) String | 加密:把明文密码 rawPassword 编码成加密的暗文密码 |
matches(CharSequence rawPassword, String encodedPassword) boolean | 校验:比较前端传入的明文密码 rawPassword 与数据库中加密后的暗文密码 encodedPassword ,匹配返回true;否则返回false |
接下来我们分别来测试这两个方法。
首先我们测试方法 encode()
,加密 2 次相同的明文密码,查看加密后的暗文情况。
import org.junit.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@SpringBootTest
public class BCryptPasswordEncoderTest {
@Test
public void testBCryptPasswordEncoder() {
// 创建BCryptPasswordEncoder对象
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
// 加密
String encodedPassword = passwordEncoder.encode("ay123456");
String encodedPassword2 = passwordEncoder.encode("ay123456");
System.out.println(encodedPassword);
System.out.println(encodedPassword2);
}
}
输出:
$2a$10$8H0a2J2sH0pF9RODN.u/k.YSedkvo5QT57mqtqxQIjCZtpRbBhknK
$2a$10$oD73ALotomxrEFuZFzRruOs17f8QNMnInl4d3CRd72dv3aw2LRd.S
可以看到, BCryptPasswordEncoder
2 次加密相同密码生成的暗文都是不同的。这是因为 encode()
方法加密的过程中,会生成一个随机的【盐】(就是暗文中开头的 $2a$10$
) 。然后,用【盐】和明文进行一系列处理后,再进行加密,这样,即使明文一样,但是每次生成的密文都是不同的。
在用户注册的时候,我们只需调用 BCryptPasswordEncoder
的 encode()
方法加密明文,把生成的密文存进 MySQL 数据库中即可。
接下来我们测试校验方法 matches()
,用上面 2 次生成的不同密文与相同的明文密码校验。
import org.junit.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@SpringBootTest
public class BCryptPasswordEncoderTest {
@Test
public void testBCryptPasswordEncoder() {
// 创建BCryptPasswordEncoder对象
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
// 校验
boolean isMatch = passwordEncoder.matches("ay123456",
"$2a$10$8H0a2J2sH0pF9RODN.u/k.YSedkvo5QT57mqtqxQIjCZtpRbBhknK");
boolean isMatch2 = passwordEncoder.matches("ay123456",
"$2a$10$oD73ALotomxrEFuZFzRruOs17f8QNMnInl4d3CRd72dv3aw2LRd.S");
System.out.println(isMatch);
System.out.println(isMatch2);
}
}
输出:
true
true
发现 2 个不同的密文都可以校验通过。由于我们已经使用 @Bean
注解往 Spring 容器里注入了 BCryptPasswordEncoder
对象,因此 Spring Security 会默认优先使用 BCryptPasswordEncoder
的加密和校验方法。在登录时,Spring Security 就会自动调用 BCryptPasswordEncoder
的 matches()
方法来校验前端传入的明文密码和 MySQL 数据库中的密文密码。
我们现在可以手动地把 MySQL 数据库中的明文密码修改成使用 BCryptPasswordEncoder
的 encode()
方法生成的暗文。
然后启动服务,浏览器访问 localhost/blog/article/352153535
,使用用户名和明文登录密码测试。
至此,Spring Security 登录时报错 java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
的问题就成功解决了。希望本文对你有帮助。