【SpingSecurity】解决lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id “null“


本期目录

  • 问题背景
  • 原因
  • 解决方法
  • BCryptPasswordEncoder介绍
  • 测试验证



问题背景

我在使用 Spring Security 时,自定义了 UserDetailsService 接口的实现类,需要实现 loadUserByUsername() 方法,把原来默认从内存中查询用户信息,修改成从 MySQL 数据库中查询用户名和密码。这样 Spring Security 就会自动使用我们自定义的 UserDetailsService 实现类进行用户的查询。将查询到的 User 对象封装成 UserDetails 的实现类 LoginUser 对象返回。

然后,我启动服务,在浏览器访问目标 URL 时,确实能正常被拦截在登录页面。正当我输入数据库 user 表里的用户名和明文密码后,点击【Sign in】后,

【SpingSecurity】解决lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id “null“_第1张图片却又跳转回了这个页面。

【SpingSecurity】解决lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id “null“_第2张图片
前往 IDEA 控制台查看日志,发现控制台报错:

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

【SpingSecurity】解决lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id “null“_第3张图片

原因

之所以会出现这种情况,是因为 Spring Security 会使用一个默认的 PasswordEncoder ,用作密码校验的编解码工具。而这个默认的 PasswordEncoder 要求 MySQL 数据库中密码字段值加上大括号 {} 前缀,大括号里面填写编码标识。

举个例子,如果 MySQL 数据库中密码字段值是明文 (即没有加密的) ,则需要在密码前面加上前缀 {noop} ,表示密码没有经过编码器加密,是明文存储的。

【SpingSecurity】解决lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id “null“_第4张图片

加上前缀 {noop} 后,可以不用重启项目,再次使用 MySQL 数据库中的用户名与密码就能成功登录了。

【SpingSecurity】解决lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id “null“_第5张图片

【SpingSecurity】解决lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id “null“_第6张图片

但以上方法有 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介绍

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$ ) 。然后,用【盐】和明文进行一系列处理后,再进行加密,这样,即使明文一样,但是每次生成的密文都是不同的。

在用户注册的时候,我们只需调用 BCryptPasswordEncoderencode() 方法加密明文,把生成的密文存进 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 就会自动调用 BCryptPasswordEncodermatches() 方法来校验前端传入的明文密码和 MySQL 数据库中的密文密码。


测试验证

我们现在可以手动地把 MySQL 数据库中的明文密码修改成使用 BCryptPasswordEncoderencode() 方法生成的暗文。

【SpingSecurity】解决lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id “null“_第7张图片

然后启动服务,浏览器访问 localhost/blog/article/352153535 ,使用用户名和明文登录密码测试。

【SpingSecurity】解决lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id “null“_第8张图片

【SpingSecurity】解决lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id “null“_第9张图片

至此,Spring Security 登录时报错 java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null" 的问题就成功解决了。希望本文对你有帮助。

你可能感兴趣的:(#,Spring,spring,java,后端,系统安全,安全架构)