Spring Security 核心过滤器链分析

7,318 阅读7分钟

前言:

在熟悉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的认证体系中则扮演着至关重要的角色