【SpringSecurity系列02】SpringSecurity 表单认证逻辑源码解读

34次阅读

共计 10831 个字符,预计需要花费 28 分钟才能阅读完成。

概要
前面一节,通过简单配置即可实现 SpringSecurity 表单认证功能,而今天这一节将通过阅读源码的形式来学习 SpringSecurity 是如何实现这些功能, 前方高能预警,本篇分析源码篇幅较长。
<!– more –>
过滤器链
前面我说过 SpringSecurity 是基于过滤器链的形式,那么我解析将会介绍一下具体有哪些过滤器。

Filter Class
介绍

SecurityContextPersistenceFilter
判断当前用户是否登录

CrsfFilter
用于防止 csrf 攻击

LogoutFilter
处理注销请求

UsernamePasswordAuthenticationFilter
处理表单登录的请求 (也是我们今天的主角)

BasicAuthenticationFilter
处理 http basic 认证的请求

由于过滤器链中的过滤器实在太多,我没有一一列举,调了几个比较重要的介绍一下。
通过上面我们知道 SpringSecurity 对于表单登录的认证请求是交给了 UsernamePasswordAuthenticationFilter 处理的,那么具体的认证流程如下:

从上图可知,UsernamePasswordAuthenticationFilter 继承于抽象类 AbstractAuthenticationProcessingFilter。
具体认证是:

进入 doFilter 方法,判断是否要认证,如果需要认证则进入 attemptAuthentication 方法,如果不需要直接结束
attemptAuthentication 方法中根据 username 跟 password 构造一个 UsernamePasswordAuthenticationToken 对象 (此时的 token 是未认证的),并且将它交给 ProviderManger 来完成认证。
ProviderManger 中维护这一个 AuthenticationProvider 对象列表,通过遍历判断并且最后选择 DaoAuthenticationProvider 对象来完成最后的认证。
DaoAuthenticationProvider 根据 ProviderManger 传来的 token 取出 username,并且调用我们写的 UserDetailsService 的 loadUserByUsername 方法从数据库中读取用户信息,然后对比用户密码,如果认证通过,则返回用户信息也是就是 UserDetails 对象,在重新构造 UsernamePasswordAuthenticationToken(此时的 token 是 已经认证通过了的)。

接下来我们将通过源码来分析具体的整个认证流程。
AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter 是一个抽象类。所有的认证认证请求的过滤器都会继承于它,它主要将一些公共的功能实现,而具体的验证逻辑交给子类实现,有点类似于父类设置好认证流程,子类负责具体的认证逻辑,这样跟设计模式的模板方法模式有点相似。
现在我们分析一下 它里面比较重要的方法
1、doFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
// 省略不相干代码。。。
// 1、判断当前请求是否要认证
if (!requiresAuthentication(request, response)) {
// 不需要直接走下一个过滤器
chain.doFilter(request, response);
return;
}
try {
// 2、开始请求认证,attemptAuthentication 具体实现给子类,如果认证成功返回一个认证通过的 Authenticaion 对象
authResult = attemptAuthentication(request, response);
if (authResult == null) {
return;
}
// 3、登录成功 将认证成功的用户信息放入 session SessionAuthenticationStrategy 接口,用于扩展
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
//2.1、发生异常,登录失败,进入登录失败 handler 回调
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
//2.1、发生异常,登录失败,进入登录失败处理器
unsuccessfulAuthentication(request, response, failed);
return;
}
// 3.1、登录成功,进入登录成功处理器。
successfulAuthentication(request, response, chain, authResult);
}
2、successfulAuthentication
登录成功处理器
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
//1、登录成功 将认证成功的 Authentication 对象存入 SecurityContextHolder 中
// SecurityContextHolder 本质是一个 ThreadLocal
SecurityContextHolder.getContext().setAuthentication(authResult);
//2、如果开启了记住我功能,将调用 rememberMeServices 的 loginSuccess 将生成一个 token
// 将 token 放入 cookie 中这样 下次就不用登录就可以认证。具体关于记住我 rememberMeServices 的相关分析我 们下面几篇文章会深入分析的。
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event
//3、发布一个登录事件。
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
//4、调用我们自己定义的登录成功处理器,这样也是我们扩展得知登录成功的一个扩展点。
successHandler.onAuthenticationSuccess(request, response, authResult);
}
3、unsuccessfulAuthentication
登录失败处理器
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
//1、登录失败,将 SecurityContextHolder 中的信息清空
SecurityContextHolder.clearContext();
//2、关于记住我功能的登录失败处理
rememberMeServices.loginFail(request, response);
//3、调用我们自己定义的登录失败处理器,这里可以扩展记录登录失败的日志。
failureHandler.onAuthenticationFailure(request, response, failed);
}
关于 AbstractAuthenticationProcessingFilter 主要分析就到这。我们可以从源码中知道,当请求进入该过滤器中具体的流程是

判断该请求是否要被认证
调用 attemptAuthentication 方法开始认证,由于是抽象方法具体认证逻辑给子类
如果登录成功,则将认证结果 Authentication 对象根据 session 策略写入 session 中,将认证结果写入到 SecurityContextHolder, 如果开启了记住我功能,则根据记住我功能,生成 token 并且写入 cookie 中,最后调用一个 successHandler 对象的方法,这个对象可以是我们配置注入的,用于处理我们的自定义登录成功的一些逻辑(比如记录登录成功日志等等)。
如果登录失败,则清空 SecurityContextHolder 中的信息,并且调用我们自己注入的 failureHandler 对象,处理我们自己的登录失败逻辑。

UsernamePasswordAuthenticationFilter
从上面分析我们可以知道,UsernamePasswordAuthenticationFilter 是继承于 AbstractAuthenticationProcessingFilter,并且实现它的 attemptAuthentication 方法,来实现认证具体的逻辑实现。接下来,我们通过阅读 UsernamePasswordAuthenticationFilter 的源码来解读,它是如何完成认证的。由于这里会涉及 UsernamePasswordAuthenticationToken 对象构造,所以我们先看看 UsernamePasswordAuthenticationToken 的源码
1、UsernamePasswordAuthenticationToken
// 继承至 AbstractAuthenticationToken
// AbstractAuthenticationToken 主要定义一下在 SpringSecurity 中 toke 需要存在一些必须信息
// 例如权限集合 Collection<GrantedAuthority> authorities; 是否认证通过 boolean authenticated = false; 认证通过的用户信息 Object details;
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {

// 未登录情况下 存的是用户名 登录成功情况下存的是 UserDetails 对象
private final Object principal;
// 密码
private Object credentials;

/**
* 构造函数,用户没有登录的情况下,此时的 authenticated 是 false,代表尚未认证
*/
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}

/**
* 构造函数,用户登录成功的情况下,多了一个参数 是用户的权限集合,此时的 authenticated 是 true,代表认证成功
*/
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // must use super, as we override
}
}
接下来我们就可以分析 attemptAuthentication 方法了。
2、attemptAuthentication
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// 1、判断是不是 post 请求,如果不是则抛出 AuthenticationServiceException 异常,注意这里抛出的异常都在 AbstractAuthenticationProcessingFilter#doFilter 方法中捕获,捕获之后会进入登录失败的逻辑。
if (postOnly && !request.getMethod().equals(“POST”)) {
throw new AuthenticationServiceException(
“Authentication method not supported: ” + request.getMethod());
}
// 2、从 request 中拿用户名跟密码
String username = obtainUsername(request);
String password = obtainPassword(request);
// 3、非空处理,防止 NPE 异常
if (username == null) {
username = “”;
}
if (password == null) {
password = “”;
}
// 4、除去空格
username = username.trim();
// 5、根据 username 跟 password 构造出一个 UsernamePasswordAuthenticationToken 对象 从上文分析可知道,此时的 token 是未认证的。
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// 6、配置一下其他信息 ip 等等
setDetails(request, authRequest);
// 7、调用 ProviderManger 的 authenticate 的方法进行具体认证逻辑
return this.getAuthenticationManager().authenticate(authRequest);
}
ProviderManager
维护一个 AuthenticationProvider 列表,进行认证逻辑验证
1、authenticate
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
// 1、拿到 token 的类型。
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
// 2、遍历 AuthenticationProvider 列表
for (AuthenticationProvider provider : getProviders()) {
// 3、AuthenticationProvider 不支持当前 token 类型,则直接跳过
if (!provider.supports(toTest)) {
continue;
}

try {
// 4、如果 Provider 支持当前 token,则交给 Provider 完成认证。
result = provider.authenticate(authentication);

}
catch (AccountStatusException e) {

throw e;
}
catch (InternalAuthenticationServiceException e) {
throw e;
}
catch (AuthenticationException e) {
lastException = e;
}
}
// 5、登录成功 返回登录成功的 token
if (result != null) {
eventPublisher.publishAuthenticationSuccess(result);
return result;
}

}
AbstractUserDetailsAuthenticationProvider
1、authenticate
AbstractUserDetailsAuthenticationProvider 实现了 AuthenticationProvider 接口,并且实现了部分方法,DaoAuthenticationProvider 继承于 AbstractUserDetailsAuthenticationProvider 类,所以我们先来看看 AbstractUserDetailsAuthenticationProvider 的实现。
public abstract class AbstractUserDetailsAuthenticationProvider implements
AuthenticationProvider, InitializingBean, MessageSourceAware {

// 国际化处理
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

/**
* 对 token 一些检查,具体检查逻辑交给子类实现,抽象方法
*/
protected abstract void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException;

/**
* 认证逻辑的实现,调用抽象方法 retrieveUser 根据 username 获取 UserDetails 对象
*/
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {

// 1、获取 usernmae
String username = (authentication.getPrincipal() == null) ? “NONE_PROVIDED”
: authentication.getName();

// 2、尝试去缓存中获取 UserDetails 对象
UserDetails user = this.userCache.getUserFromCache(username);
// 3、如果为空,则代表当前对象没有缓存。
if (user == null) {
cacheWasUsed = false;
try {
//4、调用 retrieveUser 去获取 UserDetail 对象,为什么这个方法是抽象方法大家很容易知道,如果 UserDetail 信息存在关系数据库 则可以重写该方法并且去关系数据库获取用户信息,如果 UserDetail 信息存在其他地方,可以重写该方法用其他的方法去获取用户信息,这样丝毫不影响整个认证流程,方便扩展。
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {

// 捕获异常 日志处理 并且往上抛出,登录失败。
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
“AbstractUserDetailsAuthenticationProvider.badCredentials”,
“Bad credentials”));
}
else {
throw notFound;
}
}
}

try {
// 5、前置检查 判断当前用户是否锁定,禁用等等
preAuthenticationChecks.check(user);
// 6、其他的检查,在 DaoAuthenticationProvider 是检查密码是否一致
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {

}

// 7、后置检查,判断密码是否过期
postAuthenticationChecks.check(user);

// 8、登录成功通过 UserDetail 对象重新构造一个认证通过的 Token 对象
return createSuccessAuthentication(principalToReturn, authentication, user);
}

protected Authentication createSuccessAuthentication(Object principal,
Authentication authentication, UserDetails user) {
// 调用第二个构造方法,构造一个认证通过的 Token 对象
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
principal, authentication.getCredentials(),
authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());

return result;
}

}
接下来我们具体看看 retrieveUser 的实现,没看源码大家应该也可以知道,retrieveUser 方法应该是调用 UserDetailsService 去数据库查询是否有该用户,以及用户的密码是否一致。
DaoAuthenticationProvider
DaoAuthenticationProvider 主要是通过 UserDetailService 来获取 UserDetail 对象。
1、retrieveUser
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
try {
// 1、调用 UserDetailsService 接口的 loadUserByUsername 方法获取 UserDeail 对象
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
// 2、如果 loadedUser 为 null 代表当前用户不存在,抛出异常 登录失败。
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
“UserDetailsService returned null, which is an interface contract violation”);
}
// 3、返回查询的结果
return loadedUser;
}
}
2、additionalAuthenticationChecks
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
// 1、如果密码为空,则抛出异常、
if (authentication.getCredentials() == null) {
throw new BadCredentialsException(messages.getMessage(
“AbstractUserDetailsAuthenticationProvider.badCredentials”,
“Bad credentials”));
}

// 2、获取用户输入的密码
String presentedPassword = authentication.getCredentials().toString();

// 3、调用 passwordEncoder 的 matche 方法 判断密码是否一致
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug(“Authentication failed: password does not match stored value”);

// 4、如果不一致 则抛出异常。
throw new BadCredentialsException(messages.getMessage(
“AbstractUserDetailsAuthenticationProvider.badCredentials”,
“Bad credentials”));
}
}
总结
至此,整认证流程已经分析完毕,大家如果有什么不懂可以关注我的公众号一起讨论。
学习是一个漫长的过程,学习源码可能会很困难但是只要努力一定就会有获取,大家一致共勉。

正文完
 0