文章目录
对于前面几篇的学习应该能学到很多的功能点,但也只是知道如何实现而已,关于底层完全是个黑盒状态,而且对于这些知识点都很碎片化,这一篇就来解析一下认证流程的底层源码,将整个认证流程串联起来。
一、知识回顾
认证流程入门实践:
- Spring Security(一):RESTful API的拦截之过滤器、拦截器、切片介绍
- Spring Security(二):Spring Security的初始搭建以及基本原理
- Spring Security(三):用户自定义认证逻辑
- Spring Security(四):个性化用户认证流程
功能总结
功能 | 实现 |
---|---|
处理用户信息的获取逻辑 | 实现UserDetailsService接口 |
处理用户校验逻辑 | 实现UserDetails接口 |
密码的加解密 | 实现PasswordEncoder接口 |
自定义登录成功处理 | 实现AuthenticationSuccessHandler接口 |
自定义登陆失败处理 | 实现AuthenticationFailHandler接口 |
二、认证处理流程源码
基本原理
- Spring Security基本原理图
认证流程
- 认证流程图
- 第一步:首先进入
UsernamePasswordAuthenticationFilter
,得到前端传过来的用户名和密码,然后拿用户名密码构建了UsernamePasswordAuthenticationToken
对象(未授权),其类是Authentication
的一个实现,该接口封装了认证信息,最后AuthenticationManager
调用了authenticate
方法,即图中的第二步,其作用是用来管理第三步的AuthenticationProvider
。
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);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
- 第二步:进入
authenticate
方法所在的类ProviderManager
,其实现了AuthenticationManager
接口,方法里边有一个for循环,拿到所有的AuthenticationProvider
,为什么是循环呢,是因为不同的认证逻辑是不一样的,AuthenticationManager
负责把所有认证请求收集起来,然后每个去循环判断是否支持方法传进来的登录方式,如果支持的话就会进行一个认证处理,即调用provider的authenticate
方法。
while(var6.hasNext()) {
AuthenticationProvider provider = (AuthenticationProvider)var6.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 (AccountStatusException var11) {
this.prepareException(var11, authentication);
throw var11;
} catch (InternalAuthenticationServiceException var12) {
this.prepareException(var12, authentication);
throw var12;
} catch (AuthenticationException var13) {
lastException = var13;
}
}
}
- 第三步:实际上调用的是
AbstractUserDetailsAuthenticationProvider
这个抽象类的方法,该方法里边调用了一个retrieveUser
的方法,他被类DaoAuthenticationProvider
进行了实现,这个方法里面有一句this.getUserDetailsService().loadUserByUsername(username);
,实际上就是调用了我们UserDetailsService
这个接口的实现来获取UserDetails
,这就和我们自定义认证逻辑那一块衔接上了。
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
UserDetails loadedUser;
try {
loadedUser = this.getUserDetailsService().loadUserByUsername(username);
} catch (UsernameNotFoundException var6) {
if (authentication.getCredentials() != null) {
String presentedPassword = authentication.getCredentials().toString();
this.passwordEncoder.isPasswordValid(this.userNotFoundEncodedPassword, presentedPassword, (Object)null);
}
throw var6;
} catch (Exception var7) {
throw new InternalAuthenticationServiceException(var7.getMessage(), var7);
}
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
} else {
return loadedUser;
}
}
- 第四步:上一步调用了我们自己的
loadUserByUsername
方法,并返回到第三步,得到了UserDetails
对象;继续往下走,执行this.preAuthenticationChecks.check(user);
进行一个预检查,里面的逻辑就是判断UserDetails
的一些bool属性,包括账户是否锁定、是否可用、是否过期,如果满足某一个的话就会抛出对应的异常。预检查之后会调用additionalAuthenticationChecks
方法进行附加的检查,附加检查里面主要检查密码是否匹配。预检查之后最后还会进行this.postAuthenticationChecks.check(user);
后检查,这个检查就是判断UserDetails
最后一个bool属性,检查认证信息是否过期。
try {
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
} catch (AuthenticationException var7) {
if (!cacheWasUsed) {
throw var7;
}
cacheWasUsed = false;
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
}
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return this.createSuccessAuthentication(principalToReturn, authentication, user);
- 第五步:在所有检查之后,会根据
authentication
和user
创建一个Success的Authentication
,里边重新创建了一个UsernamePasswordAuthenticationToken
(已授权),只不过和之前不一样的是,调用的是三个参数的构造函数了,里面会传入授权信息,已经设置属性已认证。
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
- 第六步:回到
AbstractAuthenticationProcessingFilter
的doFilter
方法,在调用了attemptAuthentication
方法走了以上流程拿到认证成功的Authentication
之后,会调用successfulAuthentication
方法,里面最后一步this.successHandler.onAuthenticationSuccess(request, response, authResult);
实际上这个就是在调用自己写的登录成功处理器了。
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult);
}
SecurityContextHolder.getContext().setAuthentication(authResult);
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
- 第七步:只要以上某一步抛出了异常,异常会被捕获,捕获后会调用
this.unsuccessfulAuthentication(request, response, var8);
,里面的处理逻辑this.failureHandler.onAuthenticationFailure(request, response, failed);
就是在调用自己写的登录失败处理器了。
Authentication authResult;
try {
authResult = this.attemptAuthentication(request, response);
if (authResult == null) {
return;
}
this.sessionStrategy.onAuthentication(authResult, request, response);
} catch (InternalAuthenticationServiceException var8) {
this.logger.error("An internal error occurred while trying to authenticate the user.", var8);
this.unsuccessfulAuthentication(request, response, var8);
return;
} catch (AuthenticationException var9) {
this.unsuccessfulAuthentication(request, response, var9);
return;
}
三、认证结果如何在多个请求之间共享
思考?
- 首先在多个请求之间共享肯定是放到session里的,Spring Security是什么时候把什么东西放到了session里,什么时候从session里读出来的?
认证信息存取
- 认证流程后续图
- 实际上在上面认证成功之后的
successfulAuthentication
方法里,这一行SecurityContextHolder.getContext().setAuthentication(authResult);
就是把认证成功的Authentication
保存到SecurityContext
接口的实现类SecurityContextImpl
中,再保存到SecurityContextHolder
。 - 那么谁来用
SecurityContextHolder
呢?其实就是最后一步的SecurityContextPersistenceFilter
过滤器,它在我们之前提到的过滤器连的最前面;它的作用有两个,当请求进来,先进这个过滤器,检查session是否有SecurityContext
,如果有就把它从session里拿出来放到线程里;当过滤器链走完,请求出去时,又经过这个过滤器,这时会检查线程是否有SecurityContext
,有的话就拿出来放到session里边。 - 这样的话,不同请求就可以在session中拿到相同的认证信息,拿到以后放到线程里边,因为整个请求都是在一个线程里边完成。所以在线程的任何位置随时可以使用
SecurityContextHolder.getContext()
来获取到认证信息。 - 基本原理完整图
四、获取认证用户信息
方式一
@GetMapping("/me")
public Object getCurrentUser() {
return SecurityContextHolder.getContext().getAuthentication();
}
方式二
@GetMapping("/me")
public Object getCurrentUser(Authentication authentication) {
return authentication;
}
只获取UserDetails
@GetMapping("/me")
public Object getCurrentUser(@AuthenticationPrincipal UserDetails userDetails) {
return userDetails;
}
五、推荐阅读
实战篇
- 《手把手教你如何使用Spring Security(上):登录授权》
- 《手把手教你如何使用Spring Security(中):接口认证》
- 《手把手教你如何使用Spring Security(下):访问控制》
源码篇
- 《Spring Security源码(一):整体框架设计》
- 《Spring Security源码(二):建造者详解》
- 《Spring Security源码(三):HttpSecurity详解》
- 《Spring Security源码(四):配置器详解》
- 《Spring Security源码(五):FilterChainProxy是如何创建的?》
- 《Spring Security源码(六):FilterChainProxy是如何运行的?》
- 《Spring Security源码(七):设计模式在框架中的应用》
- 《Spring Security源码(八):登录认证源码流程》
- 《Spring Security源码(九):过滤器链上的过滤器是如何排序的?》
- 《Spring Security源码(十):权限访问控制是如何做到的?》