Spring Security 核心過濾器鏈分析

前言:java

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

1、Filter Chain 圖解

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

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進行查看:api

Debug查看

2、過濾器逐一解析

在解析前,先說說兩個相當重要的類: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相關功能:session

  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相關功能:app

  • 往該請求的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相關功能:async

  • 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相關功能:ide

  • 匹配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的認證體系中則扮演着相當重要的角色
相關文章
相關標籤/搜索