赞
踩
在上一篇《SpringBoot集成Spring Security(1)——登录认证》中已经做了Spring Security的基本入门,可以登录和做角色校验,这其中有一点比较好奇的就是密码校对这块。
下面通过源码简单的来了解下Spring Security的密码校对这块,在上一篇博客中代码示例里的SecurityConfig里面,我们自己配置了一个密码编码器,然后在检验过程中就会获取改密码编码器,拿到数据库中该用户的密码和你前端传进来的密码,调用matches方法进行校验。那具体是在哪调用的?
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(loginUserDetailsService).passwordEncoder(passwordEncoder()); } @Bean PasswordEncoder passwordEncoder(){ return new PasswordEncoder() { @Override public String encode(CharSequence charSequence) { return charSequence.toString(); } @Override public boolean matches(CharSequence charSequence, String s) { return s.equals(charSequence.toString()); } }; }
密码校验的实现是通过UsernamePasswordAuthenticationFilter这个过滤器捕获到登录请求,获取到前端传来的用户名、密码,代码如下:
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username"; public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password"; private String usernameParameter = "username"; private String passwordParameter = "password"; private boolean postOnly = true; public UsernamePasswordAuthenticationFilter() { super(new AntPathRequestMatcher("/login", "POST")); } public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (this.postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } else { String username = this.obtainUsername(request); String password = this.obtainPassword(request); // 省略.... username = username.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); this.setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } } @Nullable protected String obtainPassword(HttpServletRequest request) { return request.getParameter(this.passwordParameter); } @Nullable protected String obtainUsername(HttpServletRequest request) { return request.getParameter(this.usernameParameter); } // 省略.... }
看到UsernamePasswordAuthenticationFilter.attemptAuthentication
方法里面的这一句return this.getAuthenticationManager().authenticate(authRequest);
,getAuthenticationManager()获取ProviderManager对象,然后调用其authenticate方法:
public Authentication authenticate(Authentication authentication) throws AuthenticationException { //省略.... while(var8.hasNext()) { AuthenticationProvider provider = (AuthenticationProvider)var8.next(); if (provider.supports(toTest)) { if (debug) { logger.debug("Authentication attempt using " + provider.getClass().getName()); } try { result = provider.authenticate(authentication); // 这里 if (result != null) { this.copyDetails(authentication, result); break; } } catch (InternalAuthenticationServiceException | AccountStatusException var13) { this.prepareException(var13, authentication); throw var13; } catch (AuthenticationException var14) { lastException = var14; } } } //省略.... }
在provider.authenticate(authentication)
这里会调用到DaoAuthenticationProvider父类AbstractUserDetailsAuthenticationProvider的authenticate方法:
这里的provider是DaoAuthenticationProvider对象
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware { //省略.... public Authentication authenticate(Authentication authentication) throws AuthenticationException { //省略.... UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { // 这里会调用到我们自己实现的LoginUserDetailsService的loadUserByUsername方法,通过用户名查询数据库中用户数据 user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication); } catch (UsernameNotFoundException var6) { //省略.... } //省略.... } try { this.preAuthenticationChecks.check(user); // 校验密码 this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication); } catch (AuthenticationException var7) { } //省略.... } }
然后在this.additionalAuthenticationChecks(),这里的additionalAuthenticationChecks()是在DaoAuthenticationProvider中实现的:
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
} else {
// 获取页面填写的密码
String presentedPassword = authentication.getCredentials().toString();
// passwordEncoder当前的密码编码器,调用其matches方法比较,这里就是我们最开始自己配置的那个密码编码器对象了
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
}
上面是对源码的一个跟踪,主要目的也是解答下心中对于密码在哪校验的疑惑,通过debug的过程发现,security5中如果说我们不去指定密码解析器,那么security默认生成DelegatingPasswordEncoder这个解析器对象,那么在其matches方法中校验时,因为无法获取到security自带的密码编码器,也没有自己指定,最终会抛出异常提示"There is no PasswordEncoder mapped for the id “null”。
PS:自带哪些密码编码器可以去PasswordEncoderFactories中查看,官网也可查看详细介绍。
那自定义密码编码器其实就很简单了,最上面那个其实就是一种实现方式,那最后在贴一个MD5加密的。
// MD5工具类,这个就是在网上找的一个 public class MD5Util { private static final String SALT = "test"; public static String encode(String password) { password = password + SALT; MessageDigest md5 = null; try { md5 = MessageDigest.getInstance("MD5"); } catch (Exception e) { throw new RuntimeException(e); } char[] charArray = password.toCharArray(); byte[] byteArray = new byte[charArray.length]; for (int i = 0; i < charArray.length; i++) byteArray[i] = (byte) charArray[i]; byte[] md5Bytes = md5.digest(byteArray); StringBuffer hexValue = new StringBuffer(); for (int i = 0; i < md5Bytes.length; i++) { int val = ((int) md5Bytes[i]) & 0xff; if (val < 16) { hexValue.append("0"); } hexValue.append(Integer.toHexString(val)); } return hexValue.toString(); } }
然后在SecurityConfig中:
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(loginUserDetailsService).passwordEncoder(passwordEncoder()); } @Bean PasswordEncoder passwordEncoder(){ return new PasswordEncoder() { @Override public String encode(CharSequence charSequence) { return MD5Util.encode((String)charSequence); } // charSequence:前端传过来的 // s:数据库存储的加密后的密码 @Override public boolean matches(CharSequence charSequence, String s) { return s.equals(MD5Util.encode((String)charSequence)); } }; }
如果是说直接使用Security自带的,例如BCryptPasswordEncoder:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(loginUserDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
@Bean
BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。