一、前言
在上一篇《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();
}