Spring Security 核心過濾器鏈講解

前言:

在熟悉Spring Security的使用和基本操作後,有時根據項目需求,我們需要在security原有的過濾器鏈中,添加符合我們自己的過濾器來實現功能時,我們就必須得先瞭解security的核心過濾鏈的流程和每個過濾器的各自功能,以此,我們才可以在特點的過濾器前後加入屬於我們項目需求的過濾器。

一、Filter Chain 圖解

在配置瞭spring security瞭之後,會在運行項目的時候,DefaultSecurityFilterChain會輸出相關log:

    public DefaultSecurityFilterChain(RequestMatcher requestMatcher, List<Filter> filters){
        logger.info("Creating filter chain: " + requestMatcher + ", " + filters);
        this.requestMatcher = requestMatcher;
        this.filters = new ArrayList<Filter>(filters);
    }

輸出以下Log:

[main] o.s.s.web.DefaultSecurityFilterChain     :
Creating filter chain:
org.springframework.security.web.util.matcher.AnyRequestMatcher@1,
[
    org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@184de357,
    org.springframework.security.web.context.SecurityContextPersistenceFilter@521ba38f,
    org.springframework.security.web.header.HeaderWriterFilter@77bb916f,
    org.springframework.security.web.csrf.CsrfFilter@76b305e1,
    org.springframework.security.web.authentication.logout.LogoutFilter@17c53dfb,
    org.springframework.security.web.savedrequest.RequestCacheAwareFilter@2086d469,
    org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@b1d19ff,
    org.springframework.security.web.authentication.AnonymousAuthenticationFilter@efe49ab,
    org.springframework.security.web.session.SessionManagementFilter@5a48d186,
    org.springframework.security.web.access.ExceptionTranslationFilter@273aaab7
]

也可以從Debug進行查看:

Debug查看

二、過濾器逐一解析

在解析前,先說說兩個至關重要的類:OncePerRequestFilter和GenericFilterBean,在過濾器鏈的過濾器中,或多或少間接或直接繼承到

  • OncePerRequestFilter顧名思義,能夠確保在一次請求隻通過一次filter,而不需要重復執行。
  • GenericFilterBean是javax.servlet.Filter接口的一個基本的實現類
  • GenericFilterBean將web.xml中filter標簽中的配置參數-init-param項作為bean的屬性
  • GenericFilterBean可以簡單地成為任何類型的filter的父類
  • GenericFilterBean的子類可以自定義一些自己需要的屬性
  • GenericFilterBean,將實際的過濾工作留給他的子類來完成,這就導致瞭他的子類不得不實現doFilter方法
  • GenericFilterBean不依賴於Spring的ApplicationContext,Filters通常不會直接讀取他們的容器信息(ApplicationContext concept)而是通過訪問spring容器(Spring root application context)中的service beans來獲取,通常是通過調用filter裡面的getServletContext() 方法來獲取

2.1.WebAsyncManagerIntegrationFilter

  public final class WebAsyncManagerIntegrationFilter extends OncePerRequestFilter {
        ......
    @Override
    protected void doFilterInternal(HttpServletRequest request,
        HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {
      WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
      SecurityContextCallableProcessingInterceptor securityProcessingInterceptor = (SecurityContextCallableProcessingInterceptor) asyncManager
          .getCallableInterceptor(CALLABLE_INTERCEPTOR_KEY);
      if (securityProcessingInterceptor == null) {
        asyncManager.registerCallableInterceptor(CALLABLE_INTERCEPTOR_KEY,
            new SecurityContextCallableProcessingInterceptor());
      }
      filterChain.doFilter(request, response);
    }
  }

從源碼中,我們可以分析出WebAsyncManagerIntegrationFilter相關功能:

  • 根據請求封裝獲取WebAsyncManager
  • 從WebAsyncManager獲取/註冊SecurityContextCallableProcessingInterceptor

2.2.SecurityContextPersistenceFilter

    public class SecurityContextPersistenceFilter extends GenericFilterBean {
        ......
        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());
                }
            }
            HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
                    response);
            SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
            try {
                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");
                }
            }
        }
            ......
    }

從源碼中,我們可以分析出SecurityContextPersistenceFilter相關功能:

  • 先實例SecurityContextHolder->HttpSessionSecurityContextRepository(下面以repo代替).作用:其會從Session中取出已認證用戶的信息,提高效率,避免每一次請求都要查詢用戶認證信息。
  • 根據請求和響應構建HttpRequestResponseHolder
  • repo根據HttpRequestResponseHolder加載context獲取SecurityContext
  • SecurityContextHolder將獲得到的SecurityContext設置到Context中,然後繼續向下執行其他過濾器
  • finally-> SecurityContextHolder獲取SecurityContext,然後清除,並將其和請求信息保存到repo,從請求中移除FILTER_APPLIED屬性

2.3.HeaderWriterFilter

public class HeaderWriterFilter extends OncePerRequestFilter {
    ......
    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        for (HeaderWriter headerWriter : headerWriters) {
            headerWriter.writeHeaders(request, response);
        }
        filterChain.doFilter(request, response);
    }
}

從源碼中,我們可以分析HeaderWriterFilter相關功能:

  • 往該請求的Header中添加相應的信息,在http標簽內部使用security:headers來控制

2.4.CsrfFilter

public final class CsrfFilter extends OncePerRequestFilter {
    ......
    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain filterChain)
                    throws ServletException, IOException {
        request.setAttribute(HttpServletResponse.class.getName(), response);
        CsrfToken csrfToken = this.tokenRepository.loadToken(request);
        final boolean missingToken = csrfToken == null;
        if (missingToken) {
            csrfToken = this.tokenRepository.generateToken(request);
            this.tokenRepository.saveToken(csrfToken, request, response);
        }
        request.setAttribute(CsrfToken.class.getName(), csrfToken);
        request.setAttribute(csrfToken.getParameterName(), csrfToken);
        if (!this.requireCsrfProtectionMatcher.matches(request)) {
            filterChain.doFilter(request, response);
            return;
        }
        String actualToken = request.getHeader(csrfToken.getHeaderName());
        if (actualToken == null) {
            actualToken = request.getParameter(csrfToken.getParameterName());
        }
        if (!csrfToken.getToken().equals(actualToken)) {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Invalid CSRF token found for "
                        + UrlUtils.buildFullRequestUrl(request));
            }
            if (missingToken) {
                this.accessDeniedHandler.handle(request, response,
                        new MissingCsrfTokenException(actualToken));
            }
            else {
                this.accessDeniedHandler.handle(request, response,
                        new InvalidCsrfTokenException(csrfToken, actualToken));
            }
            return;
        }
        filterChain.doFilter(request, response);
    }
        ......
}

從源碼中,我們可以分析出CsrfFilter相關功能:

  • csrf又稱跨域請求偽造,攻擊方通過偽造用戶請求訪問受信任站點。
  • 對需要驗證的請求驗證是否包含csrf的token信息,如果不包含,則報錯。這樣攻擊網站無法獲取到token信息,則跨域提交的信息都無法通過過濾器的校驗。

2.5.LogoutFilter

public class LogoutFilter extends GenericFilterBean {
    ......
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        if (requiresLogout(request, response)) {
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();
            if (logger.isDebugEnabled()) {
                logger.debug("Logging out user '" + auth
                        + "' and transferring to logout destination");
            }
            this.handler.logout(request, response, auth);
            logoutSuccessHandler.onLogoutSuccess(request, response, auth);
            return;
        }
        chain.doFilter(request, response);
    }
    ......
}

從源碼中,我們可以分析出LogoutFilter相關功能:

  • 匹配URL,默認為/logout
  • 匹配成功後則用戶退出,清除認證信息

2.6.RequestCacheAwareFilter

public class RequestCacheAwareFilter extends GenericFilterBean {
  ......
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        HttpServletRequest wrappedSavedRequest = requestCache.getMatchingRequest(
                (HttpServletRequest) request, (HttpServletResponse) response);
        chain.doFilter(wrappedSavedRequest == null ? request : wrappedSavedRequest,
                response);
    }
}

從源碼中,我們可以分析出RequestCacheAwareFilter相關功能:

通過HttpSessionRequestCache內部維護瞭一個RequestCache,用於緩存HttpServletRequest

2.7.SecurityContextHolderAwareRequestFilter

public class SecurityContextHolderAwareRequestFilter extends GenericFilterBean {
  ......
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
                throws IOException, ServletException {
            chain.doFilter(this.requestFactory.create((HttpServletRequest) req,
                    (HttpServletResponse) res), res);
    }
    ......
}

從源碼中,我們可以分析出SecurityContextHolderAwareRequestFilter相關功能:

  • 針對ServletRequest進行瞭一次包裝,使得request具有更加豐富的API

2.8.AnonymousAuthenticationFilter

public class AnonymousAuthenticationFilter extends GenericFilterBean implements InitializingBean {
        ......
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
                throws IOException, ServletException {
            if (SecurityContextHolder.getContext().getAuthentication() == null) {
                SecurityContextHolder.getContext().setAuthentication(
                        createAuthentication((HttpServletRequest) req));
                if (logger.isDebugEnabled()) {
                    logger.debug("Populated SecurityContextHolder with anonymous token: '"
                            + SecurityContextHolder.getContext().getAuthentication() + "'");
                }
            }
            else {
                if (logger.isDebugEnabled()) {
                    logger.debug("SecurityContextHolder not populated with anonymous token, as it already contained: '"
                            + SecurityContextHolder.getContext().getAuthentication() + "'");
                }
            }
            chain.doFilter(req, res);
    }
    ......
}

從源碼中,我們可以分析出AnonymousAuthenticationFilter相關功能:

  • 當SecurityContextHolder中認證信息為空,則會創建一個匿名用戶存入到SecurityContextHolder中。匿名身份過濾器,這個過濾器個人認為很重要,需要將它與UsernamePasswordAuthenticationFilter 放在一起比較理解,spring security為瞭兼容未登錄的訪問,也走瞭一套認證流程,隻不過是一個匿名的身份。
  • 匿名認證過濾器,可能有人會想:匿名瞭還有身份?個人對於Anonymous匿名身份的理解是Spirng Security為瞭整體邏輯的統一性,即使是未通過認證的用戶,也給予瞭一個匿名身份。而AnonymousAuthenticationFilter該過濾器的位置也是非常的科學的,它位於常用的身份認證過濾器(如UsernamePasswordAuthenticationFilter、BasicAuthenticationFilter、RememberMeAuthenticationFilter)之後,意味著隻有在上述身份過濾器執行完畢後,SecurityContext依舊沒有用戶信息,AnonymousAuthenticationFilter該過濾器才會有意義—-基於用戶一個匿名身份。

2.9.SessionManagementFilter

public class SessionManagementFilter extends GenericFilterBean {
        ......
        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) {
                chain.doFilter(request, response);
                return;
            }
            request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
            if (!securityContextRepository.containsContext(request)) {
                Authentication authentication = SecurityContextHolder.getContext()
                        .getAuthentication();
                if (authentication != null && !trustResolver.isAnonymous(authentication)) {
                    // The user has been authenticated during the current request, so call the
                    // session strategy
                    try {
                        sessionAuthenticationStrategy.onAuthentication(authentication,
                                request, response);
                    }
                    catch (SessionAuthenticationException e) {
                        // The session strategy can reject the authentication
                        logger.debug(
                                "SessionAuthenticationStrategy rejected the authentication object",
                                e);
                        SecurityContextHolder.clearContext();
                        failureHandler.onAuthenticationFailure(request, response, e);
                        return;
                    }
                    // Eagerly save the security context to make it available for any possible
                    // re-entrant
                    // requests which may occur before the current request completes.
                    // SEC-1396.
                    securityContextRepository.saveContext(SecurityContextHolder.getContext(),
                            request, response);
                }
                else {
                    // No security context or authentication present. Check for a session
                    // timeout
                    if (request.getRequestedSessionId() != null
                            && !request.isRequestedSessionIdValid()) {
                        if (logger.isDebugEnabled()) {
                            logger.debug("Requested session ID "
                                    + request.getRequestedSessionId() + " is invalid.");
                        }
                        if (invalidSessionStrategy != null) {
                            invalidSessionStrategy
                                    .onInvalidSessionDetected(request, response);
                            return;
                        }
                    }
                }
            }
        chain.doFilter(request, response);
    }
    ......
}

從源碼中,我們可以分析出SessionManagementFilter相關功能:

  • securityContextRepository限制同一用戶開啟多個會話的數量
  • SessionAuthenticationStrategy防止session-fixation protection attack(保護非匿名用戶)

2.10.ExceptionTranslationFilter

public class ExceptionTranslationFilter extends GenericFilterBean {
    ......
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        try {
            chain.doFilter(request, response);
            logger.debug("Chain processed normally");
        }
        catch (IOException ex) {
            throw ex;
        }
        catch (Exception ex) {
            // Try to extract a SpringSecurityException from the stacktrace
            Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
            RuntimeException ase = (AuthenticationException) throwableAnalyzer
                    .getFirstThrowableOfType(AuthenticationException.class, causeChain);
            if (ase == null) {
                ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
                        AccessDeniedException.class, causeChain);
            }
            if (ase != null) {
                handleSpringSecurityException(request, response, chain, ase);
            }
            else {
                // Rethrow ServletExceptions and RuntimeExceptions as-is
                if (ex instanceof ServletException) {
                    throw (ServletException) ex;
                }
                else if (ex instanceof RuntimeException) {
                    throw (RuntimeException) ex;
                }
                // Wrap other Exceptions. This shouldn't actually happen
                // as we've already covered all the possibilities for doFilter
                throw new RuntimeException(ex);
            }
        }
    }
    ......
}

從源碼中,我們可以分析出ExceptionTranslationFilter相關功能:

  • ExceptionTranslationFilter異常轉換過濾器位於整個springSecurityFilterChain的後方,用來轉換整個鏈路中出現的異常
  • 此過濾器的作用是處理中FilterSecurityInterceptor拋出的異常,然後將請求重定向到對應頁面,或返回對應的響應錯誤代碼

2.11.FilterSecurityInterceptor

public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
        ......
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        FilterInvocation fi = new FilterInvocation(request, response, chain);
        invoke(fi);
    }
    public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() {
        return this.securityMetadataSource;
    }
    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return this.securityMetadataSource;
    }
    public void invoke(FilterInvocation fi) throws IOException, ServletException {
            if ((fi.getRequest() != null)
                    && (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
                    && observeOncePerRequest) {
                // filter already applied to this request and user wants us to observe
                // once-per-request handling, so don't re-do security checking
                fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
            }
            else {
                // first time this request being called, so perform security checking
                if (fi.getRequest() != null) {
                    fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
                }
                InterceptorStatusToken token = super.beforeInvocation(fi);
                try {
                    fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
                }
                finally {
                    super.finallyInvocation(token);
                }
                super.afterInvocation(token, null);
            }
    }
    ......
}

從源碼中,我們可以分析出FilterSecurityInterceptor相關功能:

  • 獲取到所配置資源訪問的授權信息
  • 根據SecurityContextHolder中存儲的用戶信息來決定其是否有權限
  • 主要一些實現功能在其父類AbstractSecurityInterceptor中

2.12.UsernamePasswordAuthenticationFilter

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
        ......
        public Authentication attemptAuthentication(HttpServletRequest request,
            HttpServletResponse response) throws AuthenticationException {
            if (postOnly && !request.getMethod().equals("POST")) {
                throw new AuthenticationServiceException(
                        "Authentication method not supported: " + request.getMethod());
            }
            String username = obtainUsername(request);
            String password = obtainPassword(request);
            if (username == null) {
                username = "";
            }
            if (password == null) {
                password = "";
            }
            username = username.trim();
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                    username, password);
            // Allow subclasses to set the "details" property
            setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
    }
    ......
}

從源碼中,我們可以分析出UsernamePasswordAuthenticationFilter相關功能:

  • 表單認證是最常用的一個認證方式,一個最直觀的業務場景便是允許用戶在表單中輸入用戶名和密碼進行登錄,而這背後的UsernamePasswordAuthenticationFilter,在整個Spring Security的認證體系中則扮演著至關重要的角色

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。

推薦閱讀: