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




问题背景

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

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

image-20230111142024092却又跳转回了这个页面。

image-20230111142151312
前往 IDEA 控制台查看日志,发现控制台报错:

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

image-20230111142350498

原因

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

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

image-20230111143056139

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

image-20230111143705167

image-20230111143952069

但以上方法有 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() 方法生成的暗文。

image-20230111165604305

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

image-20230111170145688

image-20230111170326078

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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

自牧君

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值