引言
在前面学习了自定义登陆成功处理怎么做(继承SavedRequestAwareAuthenticationSuccessHandler),自定义登陆逻辑要那么做,实现各种Spring Security提供给我们的接口。但是对整体的一个过程还是有点模糊的,现在就来捋一捋,当一个用户请求进入Username Password Authentication Filter开始,到整个认证通过,沿拦截器链返回时Spring Security都做了那些工作,各个类之间是如何调用的。
认证处理流程
以上图为依据,一步一步的分析。
1)用户发起登陆请求后,首先进入的是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 = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true;
//匹配URL和Method
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
//核心方法
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
//判断是否是POST请求
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
//获取用户名和密码。实际使用的是request.getParameter()方法
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
//此时还不知道账号密码是否正确,所以先构造一个未认证的Token
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// 把请求也一同塞进Token里面
setDetails(request, authRequest);
//Token给谁处理呢?当然是给当前的AuthenticationManager
return this.getAuthenticationManager().authenticate(authRequest);
}
在attemptAuthentication()方法中:主要是先进行请求判断并获取username和password的值,然后再生成一个UsernamePasswordAuthenticationToken对象,将这个对象塞进AuthenticationManager对象并返回,注意:此时的authRequest的权限是没有任何值的。
那Token是什么鬼?为啥还有已认证和未认证的区别?来看看Token长啥样。上UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken是继承于Authentication,它是处理登录成功回调方法中的一个参数,里面包含了用户信息、请求信息等参数。
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 510L;
//认证标识
private final Object principal;
//同上
private Object credentials;
//这个构造方法用来初始化一个没有认证的Token实例
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super((Collection)null); //权限为空
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false); //认证状态为False
}
//这个构造方法用来初始化一个已经认证的Token实例,不能直接Set!!
public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
public Object getCredentials() {
return this.credentials;
}
public Object getPrincipal() {
return this.principal;
}
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
//如果是Set认证状态,就无情的给一个异常,意思是:
//不要在这里设置已认证,不要在这里设置已认证,不要在这里设置已认证
//应该从构造方法里创建,要带上用户信息和权限列表
//避免人犯错
throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
} else {
super.setAuthenticated(false);
}
}
public void eraseCredentials() {
super.eraseCredentials();
this.credentials = null;
}
}
2)下面跳转到了 ProviderManager ,该类是 AuthenticationManager 的实现类。
还记得attemptAuthentication方法的最后一行代码吗?由该行调用我们的ProviderManager.
//Token给谁处理呢?当然是给当前的AuthenticationManager
return this.getAuthenticationManager().authenticate(authRequest);
需要注意的是AuthenticationManager 本身不包含认证逻辑,其核心是用来管理所有的 AuthenticationProvider,通过交由合适的 AuthenticationProvider 来实现认证。
AuthenticationManager会注册多种AuthenticationProvider,例如UsernamePassword对应的DaoAuthenticationProvider,既然有多种选择,那怎么确定使用哪个Provider呢?
我们可以先看看AuthenticationProvider这个接口,它提供了两个方法,一个认证(具体的校验逻辑),一个supports方法,返回一个布尔值,参数是一个Class数组,这里就是根据Token的类来确定用什么Provider来处理.
public interface AuthenticationProvider {
Authentication authenticate(Authentication var1) throws AuthenticationException;
boolean supports(Class<?> var1);
}
再来看看ProviderManager类的核心方法authenticate
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled();
for (AuthenticationProvider provider : getProviders()) {
// 1.判断是否有provider支持该Authentication
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
// 2. 真正的逻辑判断
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException e) {
……
}
}
……
}
这里之所以遍历了所有的Privider,是因为不同的的登陆方式,它的逻辑是不一样的。比如我们表单登录需要认证用户名和密码,但是当我们使用三方登录时就不需要验证密码。
传统表单登录的 AuthenticationProvider 主要是由 AbstractUserDetailsAuthenticationProvider(的子类) 来进行处理的,因为传统表单登陆传进来的参数类型是UsernamePasswordAuthenticationToken,如果用第三方的登陆,比如SocialAuthenticationFilter(AbstractAuthenticationProcessingFilter的子类),它的Token的类型就不一样
Spring Security 支持多种认证逻辑,每一种认证逻辑的认证方式其实就是一种 AuthenticationProvider。通过 getProviders() 方法就能获取所有的 AuthenticationProvider,通过 provider.supports() 来判断 provider 是否支持当前的认证逻辑。
当选择好一个合适的 AuthenticationProvider 后,通过 provider.authenticate(authentication) 来让 AuthenticationProvider 进行认证。
3)根据我们目前所使用的UsernamePasswordAuthenticationToken,provider 对应的是AbstractUserDetailsAuthenticationProvider抽象类的子类DaoAuthenticationProvider,其authenticate()属于抽象类本身的方法。
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
// 1.从缓存中获取 UserDetails
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
// 2.缓存获取不到,就去接口实现类中获取
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
……
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
try {
// 3.用户信息预检查(用户是否密码过期,用户信息被删除等)
preAuthenticationChecks.check(user);
// 4.附加的检查(密码检查:匹配用户的密码和服务器中的用户密码是否一致)
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}
// 5.最后的检查
postAuthenticationChecks.check(user);
……
// 6.返回真正的经过认证的Authentication
return createSuccessAuthentication(principalToReturn, authentication, user);
}
注意:retrieveUser()的具体方法实现是由DaoAuthenticationProvider类完成的:
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
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) {
……
}
}
}
this.getUserDetailsService().loadUserByUsername方法实际上是在调用我们自己写的UserDetailsService的一个实现来获取UserDetails。这就和上篇博客《Spring Security之自定义用户认证逻辑》无缝接轨。当成功的拿到UserDetails后,就会对其进行一系列的校验,比如preAuthenticationChecks、additionalAuthenticationChecks等,检查用户是否过期(回忆UserDetails那四个布尔值)、密码是否正确····
当所有的校验都通过了后,调用 createSuccessAuthentication() 返回认证信息:
可以看到这里重新new了一个UsernamePasswordAuthenticationToken,不过这次调用是有三个参数的构造函数,因为到这里认证已经通过了,所以将 authorities用户的权限注入进去,并设置authenticated为true,表示认证已通过。
4)至此认证信息就被传递回 UsernamePasswordAuthenticationFilter 中,在 UsernamePasswordAuthenticationFilter 的父类 AbstractAuthenticationProcessingFilter 的 doFilter() 中,会根据认证的成功或者失败调用相应的 handler
这里调用的handler可以通过自己实现相应的接口来自定义。
要注意的是,在认证的过程中,只要有任何一处出现的异常,该异常都会被捕获,并交由failureHandler的onAuthenticationFailure方法来处理。
将认证结果在多个请求之间共享
完成了用户认证处理流程之后,我们思考一下是如何在多个请求之间共享这个认证结果的呢?可以联想到默认的方式应该是在session中存入了认证结果。思考:那么是什么时候存放入session中的呢?又在什么时候将认证信息从 Session 中取出来的呢?
下面将 Spring Security 的认证流程补充完整:
在上一点认证成功的 successfulAuthentication()方法中,有一行语句
SecurityContextHolder.getContext().setAuthentication(authResult);
其实就是在这里将认证信息放入 Session 中。
查看 SecurityContext 源码,发现内部就是对 Authentication 的封装,重写了 equals、hashcode、toString等方法,来保证authentication的唯一性。
而SecurityContextHolder 可以理解为线程中的 ThreadLocal,可以在不同方法之间进行通信,可以简单理解为线程级别的一个全局变量。这里简单看一下它的源码。
SecurityContextHolder类中存着 静态属性:SecurityContextHolderStrategy。而SecurityContextHolderStrategy接口的所有实现类:
非常显眼的看出:ThreadLocalSecurityContextHolderStrategy类
final class ThreadLocalSecurityContextHolderStrategy implements
SecurityContextHolderStrategy {
private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();
……
public void setContext(SecurityContext context) {
Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
// 将已认证的用户对象保存到 ThreadLocal<SecurityContext> 中
contextHolder.set(context);
}
……
}
我们知道一个 HTTP 请求和响应都是在一个线程中执行,因此在整个处理的任何一个方法中都可以通过 SecurityContextHolder.getContext()来取得存放进去的认证信息.
从 Session 中对认证信息的处理由 SecurityContextPersistenceFilter 来处理,它位于 Spring Security 过滤器链的最前面,它的主要作用是:
- 当请求时,检查 Session 中是否存在 SecurityContext,如果有将其放入到线程中。
- 当响应时,检查线程中是否存在 SecurityContext,如果有将其放入到 Session 中。
小总结:
- 用户名和密码被过滤器获取到,封装成Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。
- AuthenticationManager 身份管理器负责验证这个Authentication。
- 认证成功后,AuthenticationManager身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除)Authentication实例.
- SecurityContextHolder安全上下文容器将第3步填充了信息的Authentication,通过SecurityContextHolder.getContext().setAuthentication(…)方法,存到session中。
高度概括起来所有使用的核心认证相关接口:SecurityContextHolder是
身份信息的存放容器,Authentication是身份信息的抽象,AuthenticationManager是身份认证器,一般常用的是用户名+密码的身份认证器,还有其它认证器,如邮箱+密码、手机号码+密码等。
获取用户认证信息
通过调用 SecurityContextHolder.getContext().getAuthentication() 就能够取得认证信息,有两种方式获取.
@GetMapping("/getuser1")
public Object getCurrentUser(){
return SecurityContextHolder.getContext().getAuthentication();
}
@GetMapping("/getuser2")
@ResponseBody
public Object getMeDetail(Authentication authentication){
return authentication; //MVC自动注入
}
结果如下:
如果我们只想获取用户名和密码以及它的权限,不需要ip地址等太多的信息可以使用下面的方式来获取信息:
@GetMapping("/getuser3")
@ResponseBody
public Object getMeDetail(@AuthenticationPrincipal UserDetails userDetails){
return userDetails;
}
结果如下:
结语
🐧Life is like a box of chocolates!
Q.E.D.