@[TOC]
前言
通过[上文]了解SpringSecurity核心组件后,就可以进一步了解其认证的实现流程了。
认证入口(过滤器)
在SpringSecurity中处理认证逻辑是在UsernamePasswordAuthenticationFilter
这个过滤器中实现的。UsernamePasswordAuthenticationFilter
继承于AbstractAuthenticationProcessingFilter
这个父类。
而在UsernamePasswordAuthenticationFilter
没有实现doFilter方法,所以认证的逻辑需要先看AbstractAuthenticationProcessingFilter
中的doFilter方法。
上面的核心代码是
Authentication authenticationResult = attemptAuthentication(request, response);
attemptAuthentication
方法的作用是获取Authentication
对象对应的其实就是认证过程,进入到UsernamePasswordAuthenticationFilter
中来查看具体的实现。
@Override
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());
}
String username = obtainUsername(request);
username = (username != null) ? username : "";
username = username.trim();
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
上面代码的含义非常清晰
- 该方法只支持POST方式提交的请求
- 获取账号和密码
- 通过账号密码获取了
UsernamePasswordAuthenticationToken
对象 - 设置请求的详细信息
- 通过
AuthenticationManager
来完成认证操作
在上面的逻辑中出现了一个对象AuthenticationManager
认证管理器
AuthenticationManager
接口中就定义了一个方法authenticate
方法,处理认证的请求。
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
认证器说明
AuthenticationManager
的默认实现是ProviderManager
,而在ProviderManager
的authenticate
方法中实现的操作是循环遍历成员变量List<AuthenticationProvider> providers
。该providers
中如果有一个AuthenticationProvider
的supports
函数返回true,那么就会调用该AuthenticationProvider
的authenticate
函数认证,如果认证成功则整个认证过程结束。如果不成功,则继续使用下一个合适的AuthenticationProvider
进行认证,只要有一个认证成功则为认证成功。
在当前环境下默认的实现提供是
默认认证器的实现
进入到AbstractUserDetailsAuthenticationProvider
中的认证方法
然后进入到retrieveUser
方法中,具体的实现是DaoAuthenticationProvider
,getUserDetailsService
会获取到我们自定义的UserServiceImpl
对象,也就是会走我们自定义的认证方法了。
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
如果账号存在就会开始密码的验证,不过在密码验证前还是会完成一个检查
然后就是具体的密码验证
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
具体的验证的逻辑
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
// 密码为空
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
// 获取表单提交的密码
String presentedPassword = authentication.getCredentials().toString();
// 表单提交的密码和数据库查询的密码 比较是否相对
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
上面的逻辑会通过对应的密码编码器来处理,如果是非加密的情况会通过NoOpPasswordEncoder来处理
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return rawPassword.toString().equals(encodedPassword);
}
如果有加密处理,就选择对应的加密对象来处理,比如SpringSecurity 入门使用的BCryptPasswordEncoder来处理
总结
Spring Security认证流程:
- 请求首先会进入过滤器,它的作用在于验证系统设置受限资源的过滤器。
- 请求被过滤器接收后,会将其传递给AuthenticationProvider。AuthenticationProvider需要用户的信息以及UserDetailsService的认证实现方式。
- UserDetailsService是被UserDetail继承的,UserDetail封装了User用户信息。在这个阶段,用户信息被封装进AuthenticationProvider。
- 接着,封装了用户信息的AuthenticationProvider会被传递给AuthenticationManager中的ProviderManager。
ProviderManager会对AuthenticationProvider进行再次审核,最终返回过滤器。
通过Spring Security的认证实现,可以看到虽然代码简洁明了,但是其扩展性极强。Spring Security支持多种认证和授权机制,包括用户名密码认证、JWT令牌认证、OAuth2认证等等。同时,Spring Security还提供了多种默认配置,可以根据需要进行调整和扩展。