引言

security
在前面学习了自定义登陆成功处理怎么做(继承SavedRequestAwareAuthenticationSuccessHandler),自定义登陆逻辑要那么做,实现各种Spring Security提供给我们的接口。但是对整体的一个过程还是有点模糊的,现在就来捋一捋,当一个用户请求进入Username Password Authentication Filter开始,到整个认证通过,沿拦截器链返回时Spring Security都做了那些工作,各个类之间是如何调用的。

认证处理流程

20200203214210
以上图为依据,一步一步的分析。
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
20200205215126
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的类型就不一样
20200206162712

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() 返回认证信息:
202002061951
可以看到这里重新new了一个UsernamePasswordAuthenticationToken,不过这次调用是有三个参数的构造函数,因为到这里认证已经通过了,所以将 authorities用户的权限注入进去,并设置authenticated为true,表示认证已通过。

4)至此认证信息就被传递回 UsernamePasswordAuthenticationFilter 中,在 UsernamePasswordAuthenticationFilter 的父类 AbstractAuthenticationProcessingFilter 的 doFilter() 中,会根据认证的成功或者失败调用相应的 handler
202002031133
这里调用的handler可以通过自己实现相应的接口来自定义。
要注意的是,在认证的过程中,只要有任何一处出现的异常,该异常都会被捕获,并交由failureHandler的onAuthenticationFailure方法来处理。

将认证结果在多个请求之间共享

完成了用户认证处理流程之后,我们思考一下是如何在多个请求之间共享这个认证结果的呢?可以联想到默认的方式应该是在session中存入了认证结果。思考:那么是什么时候存放入session中的呢?又在什么时候将认证信息从 Session 中取出来的呢?
下面将 Spring Security 的认证流程补充完整:
20200206200310
在上一点认证成功的 successfulAuthentication()方法中,有一行语句

SecurityContextHolder.getContext().setAuthentication(authResult);

其实就是在这里将认证信息放入 Session 中。
查看 SecurityContext 源码,发现内部就是对 Authentication 的封装,重写了 equals、hashcode、toString等方法,来保证authentication的唯一性。

而SecurityContextHolder 可以理解为线程中的 ThreadLocal,可以在不同方法之间进行通信,可以简单理解为线程级别的一个全局变量。这里简单看一下它的源码。
20200206202511
SecurityContextHolder类中存着 静态属性:SecurityContextHolderStrategy。而SecurityContextHolderStrategy接口的所有实现类:
20200206203614
非常显眼的看出: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 中。
    20200206204622

小总结:

  • 用户名和密码被过滤器获取到,封装成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自动注入
    }

结果如下:
20200206210414

如果我们只想获取用户名和密码以及它的权限,不需要ip地址等太多的信息可以使用下面的方式来获取信息:

 @GetMapping("/getuser3")
    @ResponseBody
    public Object getMeDetail(@AuthenticationPrincipal UserDetails userDetails){
        return userDetails;
    }

结果如下:
20200206210710

结语

🐧Life is like a box of chocolates!