上一篇讲了 Security 的 Filter 是怎么运行的 , 这一篇我们来看看 Security 的认证流程 .

二 . 认证信息的流转

2.1 SecurityContext 基本对象信息

Security 核心信息就是 SecurityContext , 我们来看看认证信息是怎么确定和流转的

SecurityContextHolder 是 Spring Security 存储被验证者的详细信息的地方。Spring Security 不关心 SecurityContextHolder 是如何填充的。如果它包含一个值,则将其用作当前经过身份验证的用户。

生成一个 SecurityContext

// 表明用户已通过身份验证的最简单方法是直接设置 SecurityContextHolder
SecurityContext context = SecurityContextHolder.createEmptyContext(); 
// 生成 Authentication 认证对象
Authentication authentication =new TestingAuthenticationToken("username", "password", "ROLE_USER"); 
context.setAuthentication(authentication);
// SecurityContextHolder 中设置 context
SecurityContextHolder.setContext(context); 

获得已经认证的用户

// Step 1 : 获取 SecurityContext
SecurityContext context = SecurityContextHolder.getContext();
// Step 2 : 获取 Authentication 及其相关信息
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

SecurityContextHolder 的相关逻辑

  • 默认情况下 SecurityContextHolder 使用 ThreadLocal 来存储这些细节 (PS : 也可以通过 SecurityContextHolder.MODE global 配置)
  • 第一个是设置系统属性
  • 第二个是调用 SecurityContextHolder 上的静态方法
  • Spring Security 的 FilterChainProxy 确保 SecurityContext 总是被清除
  • // SecurityContextHolder 提供了以下的参数
    // 提供了三种不同的Mode
    public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
    public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
    public static final String MODE_GLOBAL = "MODE_GLOBAL";
    public static final String SYSTEM_PROPERTY = "spring.security.strategy";
    // 策略类型名
    private static String strategyName = System.getProperty(SYSTEM_PROPERTY);
    private static SecurityContextHolderStrategy strategy;
    private static int initializeCount = 0;
    // Node 1 : 类初始化 , 这里因为有静态初始化块 , 所以上面从才可以总结通过静态类来设置
    static {
        initialize();
    // 进行了初始化操作
    private static void initialize() {
        if (!StringUtils.hasText(strategyName)) {
            strategyName = MODE_THREADLOCAL;
        // 这里可以看到 , 有三种不同的Mode , 分别对应三种不同的策略
        if (strategyName.equals(MODE_THREADLOCAL)) {
            strategy = new ThreadLocalSecurityContextHolderStrategy();
        }else if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
            strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
        }else if (strategyName.equals(MODE_GLOBAL)) {
            strategy = new GlobalSecurityContextHolderStrategy();
        }else {
            try {
                // 反色获取
                Class<?> clazz = Class.forName(strategyName);
                Constructor<?> customStrategy = clazz.getConstructor();
                strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
            } catch (Exception ex) {
                ReflectionUtils.handleReflectionException(ex);
        initializeCount++;
    // Node 2 : setContext 逻辑 , 通过策略调用
    public static void setContext(SecurityContext context) {
        strategy.setContext(context);
    

    GlobalSecurityContextHolderStrategy : 其中 SecurityContext 就是个静态变量 InheritableThreadLocalSecurityContextHolderStrategy : 其中包含一个 ThreadLocal<SecurityContext> ThreadLocalSecurityContextHolderStrategy : 和上一个没什么区别

    2.2 SecurityContext 流程

    sequenceDiagram
    FilterChainProxy->>AbstractAuthenticationProcessingFilter: 调用 doFilter
    AbstractAuthenticationProcessingFilter-->>DatabaseAuthenticationFilter: 调用attemptAuthentication
    DatabaseAuthenticationFilter-->>AuthenticationManager: 通过 Manager 调用 provider
    AuthenticationManager-->> Provider: 调用 Provider 请求 Authentication
    Provider-->> AuthenticationManager: 然会 Authentication
    AuthenticationManager-->>DatabaseAuthenticationFilter: 返回 Authentication
    DatabaseAuthenticationFilter-->>AbstractAuthenticationProcessingFilter: 返回 Authentication
    Note right of AbstractAuthenticationProcessingFilter: 这里会调用相关Handler 最终处理 Authentication
    

    Step 1 : 调用 Provider 处理情况 , 这里认证完成后返回了一个 Authentication

    // 回忆一下 , 之前 Filter 中 , 调用 AuthenticationManager 开始了 Provider 的流程
    DatabaseUserToken authRequest = new DatabaseUserToken(username, password);
    setDetails(request, authRequest);
    return this.getAuthenticationManager().authenticate(authRequest);
    // 往外层追溯一下 , 可以看到 , 其核心被调用的是抽象类 AbstractAuthenticationProcessingFilter
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) 
    

    Step 2 : Provider 处理

    这里的 AuthenticationManager 主要是 ProviderManager 主要是这些 ,我们仅保留其中比较重要的逻辑 :

    public Authentication authenticate(Authentication authentication)
    			throws AuthenticationException {
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        Authentication result = null;
        Authentication parentResult = null;
        boolean debug = logger.isDebugEnabled();
        for (AuthenticationProvider provider : getProviders()) {
            // 每个 Provider 都会重写 supports , 此处判断是否支持该 Provider
            if (!provider.supports(toTest)) {
                continue;
            try {
                // 此处调用具体的 Provider 执行
                result = provider.authenticate(authentication);
                if (result != null) {
                    copyDetails(authentication, result);
                    break;
            }catch (AccountStatusException|InternalAuthenticationServiceException e) 
                    prepareException(e, authentication);
                    throw e;
            }catch (AuthenticationException e) {
                    lastException = e;
        // 这里还有个补偿策略 ,如果当前 AuthenticationManager 处理不了 , 会由 父类处理
        // 暂时没想清楚具体的使用场景 , 可能适用于细粒度权限这种
        if (result == null && parent != null) {	
            try {
                result = parentResult = parent.authenticate(authentication);
            }catch (AuthenticationException e) {
                lastException = e;
        if (result != null) {
            if (eraseCredentialsAfterAuthentication
    					&& (result instanceof CredentialsContainer)) {
                ((CredentialsContainer) result).eraseCredentials();
            // 发布认证成功的时间
            if (parentResult == null) {
                eventPublisher.publishAuthenticationSuccess(result);
            return result;
        if (lastException == null) {
            lastException = new ProviderNotFoundException(messages.getMessage(
    					"ProviderManager.providerNotFound",
    					new Object[] { toTest.getName() },
    					"No AuthenticationProvider found for {0}"));
        prepareException(lastException, authentication);
        throw lastException;
    

    可以看到 ,到这一步 Provider 返回了一个 Authentication 回去

    Step 3 : AbstractAuthenticationProcessingFilter 处理

    从第二步 Prodiver 返回了 Authentication , 他最终被传递到 AbstractAuthenticationProcessingFilter 中

    // AbstractAuthenticationProcessingFilter 伪代码 : 
    // Step 1 : 进行预认证
    authResult = attemptAuthentication(request, response);
    // Step 2 : Session 策略处理 , 这里的 sessionStrategy 是 NullAuthenticatedSessionStrategy , 其里面是空实现 
    sessionStrategy.onAuthentication(authResult, request, response) 
    // Step 3 : 成功后执行容器处理
    successfulAuthentication(request, response, chain, authResult)    
        |- SecurityContextHolder.getContext().setAuthentication(authResult) // 果然来了 , 把 authResult 放入 SecurityContext
        |- rememberMeServices.loginSuccess(request, response, authResult) // 记住我功能的处理 , RememberFilter 会对这个进行处理
        |- successHandler.onAuthenticationSuccess(request, response, authResult) // SavedRequestAwareAuthenticationSuccessHandler
    

    至此 , Provider 产生的 Authentication 成功放入 容器中

    扩展 SavedRequestAwareAuthenticationSuccessHandler 处理 Success 结果

    总结一下就是定制缓存和跳转关系 >>>

    C- SavedRequestAwareAuthenticationSuccessHandler
        P- RequestCache requestCache : Request 缓存工具 , 用户获取缓存的 Request 对象
        M- onAuthenticationSuccess
            - SavedRequest savedRequest = requestCache.getRequest(request, response) : 先获取缓存的对象
            - String targetUrlParameter = getTargetUrlParameter() : 这里是看看有没有成功的跳转地址 
            	?- 如果想实现不同用户不同跳转 ,定制这里
            - clearAuthenticationAttributes(request) : 删除与身份验证相关的临时数据,这些数据可能在身份验证过程中存储在会话中 , 避免敏感信息泄露
            - String targetUrl = savedRequest.getRedirectUrl();
            - getRedirectStrategy().sendRedirect(request, response, targetUrl);
    			?- 重定向出去
    

    以上是认证从和认证失败的流程图 , 可以看到具体的处理类 :

    总结一下认证成功和认证失败分别干了什么 :

    如果认证失败:

  • Security contextholder 被清空了。
  • 调用 RememberMeServices.loginFail。如果没有配置 rememberme,这是一个 no-op
  • 调用 AuthenticationFailureHandler。
  • 同时对比一下认证成功:

  • 会在新登录时通知 SessionAuthenticationStrategy
  • 在 SecurityContextHolder 上设置身份验证,然后 SecurityContextPersistenceFilter 将 SecurityContext 保存到 HttpSession 中
  • 调用 RememberMeServices.loginSuccess。如果没有配置 remember me,这是一个 no-op
  • ApplicationEventPublisher 发布交互式身份验证连接
  • 调用 AuthenticationSuccessHandler
  • 三 . 再次访问和退出

    上面说了一个认证过程中发生了什么 , 这里我们看下认证完成后再次访问>>>

    3.1 认证后访问

    // 前面说了 , 认证完成后会写入 SecurityContextHolder , Security 通过判断 SecurityContext 来校验用户
    // 同理 , 下次访问的时候同样通过该方式 :
    // Step 1 : SecurityContextPersistenceFilter 拦截到请求
    // Step 2 : 从请求中获取 SecurityContext
    SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
    // 从 HttpSessionSecurityContextRepository 中获取
    C- HttpSessionSecurityContextRepository
        SecurityContext context = readSecurityContextFromSession(httpSession);
    // 此处断点可以看到认证信息 : 
    org.springframework.security.core.context.SecurityContextImpl@45eed422: 
    Authentication: com.security.demo.token.DatabaseUserToken@45eed422: 
    Principal: gang; 
    Credentials: [PROTECTED]; 
    Authenticated: true; 
    Details: org.springframework.security.web.authentication.WebAuthenticationDetails@fffdaa08: RemoteIpAddress: 127.0.0.1; 
    SessionId: A58D946FBFCB17743E2E0A44DBAB7A76; 
    Granted Authorities: ROLE_USER	
    // Step 3 : finally 此处将 SecurityContext 进行了设置
    SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
    SecurityContextHolder.clearContext();
    repo.saveContext(contextAfterChainExecution, holder.getRequest(),holder.getResponse());
    

    PS : 因为是基于 Session 管理 , 所以过一会就过期了
    当然这是基于 Session 的模式 ,生命周期和 Session 等同 ,但是通常会常用更长的生命周期方案 ,比如 AccessToken , Cookie 等等 ,而 Session 只是为了维持一个认证的临时状态

    3.2 Logout 退出

    Logout 相关类 :

  • PersistentTokenBasedRememberMeServices
  • TokenBasedRememberMeServices
  • CookieClearingLogoutHandler
  • CsrfLogoutHandler
  • SecurityContextLogoutHandler
  • HeaderWriterLogoutHandler
  • 同样的 , Logout 也有 Filter 和 Handler

  • LogoutFilter
  • SimpleUrlLogoutSuccessHandler
  • HttpStatusReturningLogoutSuccessHandler
  • 和前面分析 Filter 一样 , 其核心还是通过 LogoutFilter 来进行 :

    this.handler.logout(request, response, auth);
    logoutSuccessHandler.onLogoutSuccess(request, response, auth);
    C- SecurityContextLogoutHandler : 核心类 , 处理 Context 
    	public void logout(HttpServletRequest request, HttpServletResponse response,
    			Authentication authentication) {
    		Assert.notNull(request, "HttpServletRequest required");
    		if (invalidateHttpSession) {
    			HttpSession session = request.getSession(false);
    			if (session != null) {
    				logger.debug("Invalidating session: " + session.getId());
    				session.invalidate();
    		if (clearAuthentication) {
                 // 此处将 SecurityContext 设置为了 null
    			SecurityContext context = SecurityContextHolder.getContext();
    			context.setAuthentication(null);
    		SecurityContextHolder.clearContext();
    

    至此 , 一个完整的 Security 生命周期就看完了, 其实很简单 , 总结起来就是 :

  • Filter 做业务决定
  • AuthenticationManager 决定校验方式
  • Provider 进行认证校验
  • Handler做结果处理已经外部跳转
  • 后面一章我们来详细看看Security 的配置逻辑 ,看看底层发生了什么