Spring Security 核心過濾器鏈分析

CatalpaFlat發表於2017-12-27

前言:

在熟悉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相關功能:

  1. 先例項SecurityContextHolder->HttpSessionSecurityContextRepository(下面以repo代替).作用:其會從Session中取出已認證使用者的資訊,提高效率,避免每一次請求都要查詢使用者認證資訊。
  2. 根據請求和響應構建HttpRequestResponseHolder
  3. repo根據HttpRequestResponseHolder載入context獲取SecurityContext
  4. SecurityContextHolder將獲得到的SecurityContext設定到Context中,然後繼續向下執行其他過濾器
  5. 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的認證體系中則扮演著至關重要的角色

相關文章