报错解决:There-is-no-PasswordEncoder-mapped-for-the-id-null

问题描述

Spring Security 5.4.1环境中新增一个usernametomuserpswd123123的用户,在登陆时出现以下的错误,但是账号密码的确和数据库中是对应的。

HTTP状态 500 - 内部服务器错误
类型 异常报告

消息 There is no PasswordEncoder mapped for the id "null"

描述 服务器遇到一个意外的情况,阻止它完成请求。

例外情况

java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
	org.springframework.security.crypto.password.DelegatingPasswordEncoder$UnmappedIdPasswordEncoder.matches(DelegatingPasswordEncoder.java:254)
	org.springframework.security.crypto.password.DelegatingPasswordEncoder.matches(DelegatingPasswordEncoder.java:202)
	org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter$LazyPasswordEncoder.matches(WebSecurityConfigurerAdapter.java:595)
	org.springframework.security.authentication.dao.DaoAuthenticationProvider.additionalAuthenticationChecks(DaoAuthenticationProvider.java:76)
	org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider.authenticate(AbstractUserDetailsAuthenticationProvider.java:147)
	org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:182)
	org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:201)
	org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.attemptAuthentication(UsernamePasswordAuthenticationFilter.java:85)
	org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:222)
	org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:212)
	org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336)
	org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:103)
	org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:89)
	org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336)
	org.springframework.security.web.csrf.CsrfFilter.doFilterInternal(CsrfFilter.java:130)
	org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
	.............

错误原因

Spring Security 5.xx版本中新增了加密方式的校验。之前版本中的NoOpPasswordEncoderDelegatingPasswordEncoder取代了,由于在新建用户的时候,没有指定密码的加密方式。但是现有版本的Spring Security会对前端传来的密码进行检查,检验密码格式是否符合格式{id}encodedPassword,在DelegatingPasswordEncoder源码中给出了不同加密的样例,都是{id}encodedPassword的格式:

// {bcrypt}:BCrypt强哈希方法
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
// {noop}:无加密
{noop}password
// {PBKDF2}:PBKDF2加密
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
// {scrypt}:scrypt加密
{scrypt}:$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
// {sha256}:sha256加密
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0

解决措施

方式1:内存中完成读取

@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
    // 基于内存
    builder
        // 在内存完成账号、密码的检查
        .inMemoryAuthentication()
        .passwordEncoder(new BCryptPasswordEncoder())
        // 指定账号
        .withUser("tom")
        // 指定密码
        .password(new BCryptPasswordEncoder().encode("123123"));
}

方式2:数据库读取方式

主要的解决方法就是,指定加密方式{id},以及将数据库中查的的密码进行加密得到encodedPassword,最后将两者拼接即可;

其中admin.getUserpswd()为我们从数据库中查得的密码。

不采用任何加密方式

此时的{id}即为{noop}encodedPassword即为admin.getUserpswd(),最终的密码格式为:

String finalPswd = "{noop}" + admin.getUserpswd();

采用BCrypt加密方式

此时的{id}即为{bcrypt}encodedPassword即为new BCryptPasswordEncoder().encode(admin.getUserpswd()),最终的密码格式为:

String finalPswd = "{bcrypt}" + new BCryptPasswordEncoder().encode(admin.getUserpswd());

其他加密方式同上处理,选择一种加密方式即可。最后存入import org.springframework.security.core.userdetails.User类中即可:

// finalPswd即为加密后的密码。
return new User(username, finalPswd, authorities);

或者还可以利用DelegatingPasswordEncoder自己提供的拼接前后缀的方式:

// 创建一个map,其中idForEncode为加密方式,对应的值为对应的加密对象
Map<String, PasswordEncoder> idToPasswordEncoder = new HashMap<>();
String idForEncode = "bcrypt";
idToPasswordEncoder.put(idForEncode, new BCryptPasswordEncoder());
// 创建DelegatingPasswordEncoder实例
DelegatingPasswordEncoder delegatingPasswordEncoder = new DelegatingPasswordEncoder(idForEncode, idToPasswordEncoder);
// 得到符合格式`{id}encodedPassword`的密码
String finalPswd = delegatingPasswordEncoder.encode(admin.getUserpswd());

DelegatingPasswordEncoder的有参构造器:

/**
 * Creates a new instance
 * @param idForEncode the id used to lookup which {@link PasswordEncoder} should be
 * used for {@link #encode(CharSequence)}
 * @param idToPasswordEncoder a Map of id to {@link PasswordEncoder} used to determine
 * which {@link PasswordEncoder} should be used for
 * {@link #matches(CharSequence, String)}
 */
public DelegatingPasswordEncoder(String idForEncode, Map<String, PasswordEncoder> idToPasswordEncoder) {
   if (idForEncode == null) {
      throw new IllegalArgumentException("idForEncode cannot be null");
   }
   if (!idToPasswordEncoder.containsKey(idForEncode)) {
      throw new IllegalArgumentException(
            "idForEncode " + idForEncode + "is not found in idToPasswordEncoder " + idToPasswordEncoder);
   }
   for (String id : idToPasswordEncoder.keySet()) {
      if (id == null) {
         continue;
      }
      if (id.contains(PREFIX)) {
         throw new IllegalArgumentException("id " + id + " cannot contain " + PREFIX);
      }
      if (id.contains(SUFFIX)) {
         throw new IllegalArgumentException("id " + id + " cannot contain " + SUFFIX);
      }
   }
   this.idForEncode = idForEncode;
   // 通过idForEncode(加密方式),获得对应的加密对象
   this.passwordEncoderForEncode = idToPasswordEncoder.get(idForEncode);
   this.idToPasswordEncoder = new HashMap<>(idToPasswordEncoder);
}

DelegatingPasswordEncoder中的前后缀常量

private static final String PREFIX = "{";
private static final String SUFFIX = "}";

DelegatingPasswordEncoder返回加密后密码的方法encode

其中idForEncode指定为bcrypt,则需要passwordEncoderForEncode的加密对象new BCryptPasswordEncoder(),调用其encode方法(BCryptPasswordEncoder类中的),得到encodedPassword,最后拼接起来得到{id}encodedPassword

@Override
public String encode(CharSequence rawPassword) {
   return PREFIX + this.idForEncode + SUFFIX + this.passwordEncoderForEncode.encode(rawPassword);
}

完整代码如下

MyUserDetailService类:

package com.yuanbaoqiang.security.config;

import com.yuanbaoqiang.entity.Admin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @description:
 * @author: YuanbaoQiang
 * @time: 2020/11/23 16:00
 */
@Component
public class MyUserDetailService implements UserDetailsService {
    Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public UserDetails loadUserByUsername(
            // 表单提交的用户名
            String username
    ) throws UsernameNotFoundException {
        Map<String, PasswordEncoder> idToPasswordEncoder = new HashMap<>();
        String idForEncode = "bcrypt";
        idToPasswordEncoder.put(idForEncode, new BCryptPasswordEncoder());
        DelegatingPasswordEncoder delegatingPasswordEncoder = new DelegatingPasswordEncoder(idForEncode, idToPasswordEncoder);

        // 根据表单提供的账号 查询User对象,并装配角色、权限等信息
        // 1. 从数据库中查询admin对象
        String sql = "SELECT id,loginacct,userpswd,username,email FROM t_admin WHERE loginacct=?";
        List<Admin> list = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Admin.class), username);
        Admin admin = list.get(0);

        // 2. 给admin设置权限 角色信息
        List<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN")); // 角色
        authorities.add(new SimpleGrantedAuthority("UPDATE")); // 权限

        // 3. 将admin和authorities封装在一起
        // 加密方式输入
        // String finalPswd = "{noop}" + admin.getUserpswd();
        // logger.warn(userpswd);

        // 采用bcrypt加密方式
        // String finalPswd = "{bcrypt}" + new BCryptPasswordEncoder().encode(admin.getUserpswd());
        // logger.warn(encodedPswd);

        String finalPswd = delegatingPasswordEncoder.encode(admin.getUserpswd());
        logger.warn(finalPswd);

        return new User(username, finalPswd, authorities);
    }
}

WebAppSecurityConfig配置类:

package com.yuanbaoqiang.security.config;

import com.alibaba.druid.sql.builder.SQLUpdateBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;
import java.io.IOException;

/**
 * @description:
 * @author: YuanbaoQiang
 * @time: 2020/11/22 14:21
 */
// 注意:这个类一定要放在自动扫描的包下,否则所有的配置都不会生效

// 将当前类标记为配置类
@Configuration
// 启用Web环境下权限控制功能
@EnableWebSecurity
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    @Autowired
    private MyUserDetailService myUserDetailService;

    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {

        // 基于内存的方式
        // builder
        //         // 在内存完成账号、密码的检查
        //         .inMemoryAuthentication()
        //         .passwordEncoder(new BCryptPasswordEncoder())
        //         // 指定账号
        //         .withUser("tom")
        //         // 指定密码
        //         .password(new BCryptPasswordEncoder().encode("123123"))
        //         // 指定当前用户的角色
        //         // cannot start with ROLE_ (it is automatically added)
        //         .roles("ADMIN","学徒")
        //
        //         .and()
        //
        //         // 指定账号
        //         .withUser("jerry")
        //         // 指定密码
        //         .password(new BCryptPasswordEncoder().encode("123123"))
        //         // 指定当前用户的权限
        //         .authorities("UPDATE","内门");

        // 基于数据库的方式
        // 装配UserDetailsService对象
        builder.userDetailsService(myUserDetailService);
    }

    @Override
    protected void configure(HttpSecurity security) throws Exception {

        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);

        security
                // 对请求进行授权
                .authorizeRequests()
                // 针对这个路径进行授权
                .antMatchers("/index.jsp")
                // 可以无条件访问
                .permitAll()
                // 针对layui目录下所有资源进行授权
                .antMatchers("/layui/**")
                .permitAll()
                // 针对level1设置路径访问要求
                .antMatchers("/level1/**")
                // 要求用户具备"学徒"的角色才可以访问
                .hasRole("学徒")
                // 针对level2设置路径访问要求
                .antMatchers("/level2/**")
                // 要求用户具备"内门"的角色才可以访问
                .hasAuthority("内门")
                .and()
                // 对请求进行授权
                .authorizeRequests()
                // 任意的请求
                .anyRequest()
                // 需要登陆以后才可以访问
                .authenticated()
                .and()
                // 使用表单形式登陆
                .formLogin()
                // 指定登陆页面
                // 关于loginPage()方法的特殊说明
                // 指定登录页的同时会影响到:“提交登陆表单的地址” “退出登陆地址” “登陆失败地址”
                /*
                * <ul>
                  <li>/index.jsp GET - the login form</li>
                  <li>/index.jsp POST - process the credentials and if valid authenticate the user</li> // 提交登陆表单
                  <li>/index.jsp?error GET - redirect here for failed authentication attempts</li>  // 登陆失败
                  <li>/index.jsp?logout GET - redirect here after successfully logging out</li> // 退出登陆
                  </ul>
                * */
                .loginPage("/index.jsp")
                // loginProcessingUrl()方法指定了登陆地址,就会覆盖我们loginPage()方法中设置的默认值
                // 提交登陆表单的地址
                .loginProcessingUrl("/do/login.html") //指定登陆页面,否则会跳转到SpringSecurity自带的页面中
                .usernameParameter("loginAcct") // 定制登陆账号的请求参数名
                .passwordParameter("userPswd") // 定制登陆密码的请求参数名
                .defaultSuccessUrl("/main.html") // 登陆成功后前往新地址
                // .and()
                // 禁用CSRF功能
                // .csrf()
                // .disable()
                .and()
                .logout()
                .logoutUrl("/do/logout.html")
                .logoutSuccessUrl("/index.jsp")
                // 指定异常处理器
                .and()
                .exceptionHandling()
                // .accessDeniedPage("/to/no/auth/page.html")
                .accessDeniedHandler(new AccessDeniedHandler() {
                    @Override
                    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
                        httpServletRequest.setAttribute("message", "抱歉!您无法访问该资源!");
                    }
                })
                .and()
                // 开启记住我功能
                .rememberMe()
                .tokenRepository(tokenRepository)
        ;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

YuanbaoQiang

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

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

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

打赏作者

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

抵扣说明:

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

余额充值