问题描述
在Spring Security 5.4.1
环境中新增一个username
为tom
,userpswd
为123123
的用户,在登陆时出现以下的错误,但是账号密码的确和数据库中是对应的。
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
版本中新增了加密方式的校验。之前版本中的NoOpPasswordEncoder被DelegatingPasswordEncoder取代了,由于在新建用户的时候,没有指定密码的加密方式。但是现有版本的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)
;
}
}