视频教程地址: https://www.bilibili.com/video/BV1kT4y1F7Tc

代码地址: https://gitee.com/crazyliyang/video-teaching

1. 从SpringSecurityDemo项目启动的DEBUG日志中Copy出来的日志信息, 如下是相关部分:

  1. o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: any request,
  2. [
  3. org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@3104351d,
  4. org.springframework.security.web.context.SecurityContextPersistenceFilter@782168b7,
  5. org.springframework.security.web.header.HeaderWriterFilter@1698d7c0,
  6. org.springframework.security.web.csrf.CsrfFilter@4e628b52,
  7. org.springframework.security.web.authentication.logout.LogoutFilter@40d10481,
  8. org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@7ac9af2a,
  9. org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@6daf2337,
  10. org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@77d18d0b,
  11. org.springframework.security.web.authentication.www.BasicAuthenticationFilter@1e6cc850,
  12. org.springframework.security.web.savedrequest.RequestCacheAwareFilter@7435a578,
  13. org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@23e44287,
  14. org.springframework.security.web.authentication.AnonymousAuthenticationFilter@7a344b65,
  15. org.springframework.security.web.session.SessionManagementFilter@87abc48,
  16. org.springframework.security.web.access.ExceptionTranslationFilter@73ba6fe6,
  17. org.springframework.security.web.access.intercept.FilterSecurityInterceptor@5c645b43
  18. ]

Spring Security的过滤器日志log打印顺序与实际配置顺序符合,也就意味着WebAsyncManagerIntegrationFilter是整个过滤器链的第一个过滤器,而FilterSecurityInterceptor则是末置的过滤器。另外通过观察过滤器的名称,和所在的包名,可以大致地分析出他们各自的作用,如UsernamePasswordAuthenticationFilter明显便是与使用用户名和密码登录相关的过滤器,而FilterSecurityInterceptor我们似乎看不出它的作用,但是其位于web.access包下,大致可以分析出他与访问限制相关, 我们对其中关键的过滤器进行一些源码分析先大致介绍下每个过滤器的作用:

  1. WebAsyncManagerIntegrationFilter Web异步管理集成过滤器, 使得异步线程可以从SecurityContextHolder中获取上下文信息 ( 异步操作我们先不关注, 先关注重点内容 )
  2. SecurityContextPersistenceFilter 两个主要职责:请求来临时,创建SecurityContext安全上下文信息,请求结束时清空SecurityContextHolder。
  3. HeaderWriterFilter (文档中并未介绍,非核心过滤器) 用来给http响应添加一些Header,比如X-Frame-Options, X-XSS-Protection*,X-Content-Type-Options.
  4. CsrfFilter 在spring4这个版本中被默认开启的一个过滤器,用于防止csrf攻击,了解前后端分离的人一定不会对这个攻击方式感到陌生,前后端使用json交互需要注意的一个问题。
  5. LogoutFilter 顾名思义,处理注销的过滤器
  6. UsernamePasswordAuthenticationFilter 这个会重点分析,表单提交了username和password,被封装成token进行一系列的认证,便是主要通过这个过滤器完成的,在表单认证的方法中,这是最最关键的过滤器。
  7. DefaultLoginPageGeneratingFilter 默认登录页面 生成器 过滤器
  8. DefaultLogoutPageGeneratingFilter 默认退出登录页面 生成器 过滤器
  9. BasicAuthenticationFilter Basic认证方式过滤器
  10. RequestCacheAwareFilter (文档中并未介绍,非核心过滤器) 内部维护了一个RequestCache,用于缓存request请求
  11. SecurityContextHolderAwareRequestFilter 此过滤器对ServletRequest进行了一次包装,使得request具有更加丰富的API
  12. AnonymousAuthenticationFilter 匿名身份过滤器,这个过滤器个人认为很重要,需要将它与UsernamePasswordAuthenticationFilter 放在一起比较理解,spring security为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名的身份。
  13. SessionManagementFilter 和session相关的过滤器,内部维护了一个SessionAuthenticationStrategy,两者组合使用,常用来防止session-fixation protection attack,以及限制同一用户开启多个会话的数量
  14. ExceptionTranslationFilter 直译成异常翻译过滤器,还是比较形象的,这个过滤器本身不处理异常,而是将认证过程中出现的异常交给内部维护的一些类去处理,具体是那些类下面详细介绍
  15. FilterSecurityInterceptor 这个过滤器决定了访问特定路径应该具备的权限,访问的用户的角色,权限是什么?访问的路径需要什么样的角色和权限?这些判断和处理都是由该类进行的。

其中标注为红色的过滤器可以被认为是 Spring Security 的核心过滤器,将在下面,一个过滤器对应一个小节来讲解。

一. SecurityContextPersistenceFilter

试想一下,如果我们不使用Spring Security,如果保存用户信息呢,大多数情况下会考虑使用Session对吧?在Spring Security中也是如此,用户在登录过一次之后,后续的访问便是通过sessionId来识别,从而认为用户已经被认证。具体在何处存放用户信息? 答案是 SecurityContextHolder; 认证相关的信息是如何被存放到其中的,便是通过 SecurityContextPersistenceFilter。在4.1概述中也提到了,SecurityContextPersistenceFilter的两个主要作用便是请求来临时,创建SecurityContext安全上下文信息和请求结束时清空SecurityContextHolder。顺带提一下:微服务的一个设计理念需要实现服务通信的无状态,而http协议中的无状态意味着不允许存在session,这可以通过setAllowSessionCreation(false) 实现,这并不意味着SecurityContextPersistenceFilter变得无用,因为它还需要负责清除用户信息。在Spring Security中,虽然安全上下文信息被存储于Session中,但我们在实际使用中不应该直接操作Session,而应当使用SecurityContextHolder。

org.springframework.security.web.context.SecurityContextPersistenceFilter

public class SecurityContextPersistenceFilter extends GenericFilterBean {

    static final String FILTER_APPLIED = "__spring_security_scpf_applied";

     //安全上下文存储的仓库
    private SecurityContextRepository repo;

    private boolean forceEagerSessionCreation = false;

    public SecurityContextPersistenceFilter() {
        this(new HttpSessionSecurityContextRepository());
    }

    public SecurityContextPersistenceFilter(SecurityContextRepository repo) {
        this.repo = repo;
    }

    // 核心过滤方法
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        if (request.getAttribute(FILTER_APPLIED) != null) {
            // ensure that filter is only applied once per request
            chain.doFilter(request, response);
            return;
        }

        final boolean debug = logger.isDebugEnabled();

        request.setAttribute(FILTER_APPLIED, Boolean.TRUE);

        if (forceEagerSessionCreation) {
            HttpSession session = request.getSession();

            if (debug && session.isNew()) {
                logger.debug("Eagerly created session: " + session.getId());
            }
        }

        //包装request,response
        HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,response);

        //从Session中获取安全上下文信息
        SecurityContext contextBeforeChainExecution = repo.loadContext(holder);

        try {
            //请求开始时,设置安全上下文信息,这样就避免了用户直接从Session中获取安全上下文信息
            SecurityContextHolder.setContext(contextBeforeChainExecution);

            chain.doFilter(holder.getRequest(), holder.getResponse());

        }
        finally {
            //请求结束后,清空安全上下文信息
            SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
            // Crucial removal of SecurityContextHolder contents - do this before anything
            // else.
            SecurityContextHolder.clearContext();
            repo.saveContext(contextAfterChainExecution, holder.getRequest(),
                    holder.getResponse());
            request.removeAttribute(FILTER_APPLIED);

            if (debug) {
                logger.debug("SecurityContextHolder now cleared, as request processing completed");
            }
        }
    }

    public void setForceEagerSessionCreation(boolean forceEagerSessionCreation) {
        this.forceEagerSessionCreation = forceEagerSessionCreation;
    }
}

过滤器一般负责核心的处理流程,而具体的业务实现,通常交给其中聚合的其他实体类,这在Filter的设计中很常见,同时也符合职责分离模式。例如存储安全上下文和读取安全上下文的工作完全委托给了HttpSessionSecurityContextRepository去处理,而这个类中也有几个方法可以稍微解读下,方便我们理解内部的工作流程

org.springframework.security.web.context.HttpSessionSecurityContextRepository

public class HttpSessionSecurityContextRepository implements SecurityContextRepository {
   // 'SPRING_SECURITY_CONTEXT'是安全上下文默认存储在Session中的键值
   public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT";
   ...
   private final Object contextObject = SecurityContextHolder.createEmptyContext();
   private boolean allowSessionCreation = true;
   private boolean disableUrlRewriting = false;
   private String springSecurityContextKey = SPRING_SECURITY_CONTEXT_KEY;

   private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();

   //从当前request中取出安全上下文,如果session为空,则会返回一个新的安全上下文
   public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
      HttpServletRequest request = requestResponseHolder.getRequest();
      HttpServletResponse response = requestResponseHolder.getResponse();
      HttpSession httpSession = request.getSession(false);
      SecurityContext context = readSecurityContextFromSession(httpSession);
      if (context == null) {
         context = generateNewContext();
      }
      ...
      return context;
   }

   ...

   public boolean containsContext(HttpServletRequest request) {
      HttpSession session = request.getSession(false);
      if (session == null) {
         return false;
      }
      return session.getAttribute(springSecurityContextKey) != null;
   }

   private SecurityContext readSecurityContextFromSession(HttpSession httpSession) {
      if (httpSession == null) {
         return null;
      }
      ...
      // Session存在的情况下,尝试获取其中的SecurityContext
      Object contextFromSession = httpSession.getAttribute(springSecurityContextKey);
      if (contextFromSession == null) {
         return null;
      }
      ...
      return (SecurityContext) contextFromSession;
   }

   //初次请求时创建一个新的SecurityContext实例
   protected SecurityContext generateNewContext() {
      return SecurityContextHolder.createEmptyContext();
   }

}

SecurityContextPersistenceFilter和HttpSessionSecurityContextRepository配合使用,构成了Spring Security整个调用链路的入口,为什么将它放在最开始的地方也是显而易见的,后续的过滤器中大概率会依赖Session信息和安全上下文信息。

UsernamePasswordAuthenticationFilter

Form表单认证是最常用的一个认证方式, 一个最直观的业务场景便是允许用户在表单中输入用户名和密码进行登录,而这背后的 UsernamePasswordAuthenticationFilter,在整个Spring Security的认证体系中则扮演着至关重要的角色。

UsernamePassAuthFilter.jpg

上述的时序图可以看出UsernamePasswordAuthenticationFilter 主要肩负起了调用身份认证器,校验身份的作用,至于认证的细节,到这里,其实Spring Security的基本流程就已经走通了。

源码分析

org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware {
    //包含了一个身份认证器
    private AuthenticationManager authenticationManager;

    //用于实现remeberMe
    private RememberMeServices rememberMeServices = new NullRememberMeServices();
    private RequestMatcher requiresAuthenticationRequestMatcher;

    //这两个Handler很关键,分别代表了认证成功和失败相应的处理器
    private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
    private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        ...
        Authentication authResult;
        try {
            //此处实际上就是调用 UsernamePasswordAuthenticationFilter 的 attemptAuthentication方法
            authResult = attemptAuthentication(request, response);
            if (authResult == null) {
                //子类未完成认证,立刻返回
                return;
            }
            sessionStrategy.onAuthentication(authResult, request, response);
        }
        //在认证过程中可以直接抛出异常,在过滤器中,就像此处一样,进行捕获
        catch (InternalAuthenticationServiceException failed) {
            //内部服务异常
            unsuccessfulAuthentication(request, response, failed);
            return;
        }
        catch (AuthenticationException failed) {
            //认证失败
            unsuccessfulAuthentication(request, response, failed);
            return;
        }
        //认证成功
        if (continueChainBeforeSuccessfulAuthentication) {
            chain.doFilter(request, response);
        }
        //注意,认证成功后过滤器把authResult结果也传递给了成功处理器
        successfulAuthentication(request, response, chain, authResult);
    }

}

UsernamePasswordAuthenticationFilter.png