Skip to content

FilterChainProxy过滤器链中的重要过滤器

SecurityContextPersistenceFilter

这个过滤器是FilterChainProxy过滤器链的中第一个调用的,先看下它的官方类注释的内容,总结为以下几点:

  1. 在请求之前,使用从已配置的SecurityContextRepository中获取的认证信息来填充SecurityContextHolder,在请求完成后清除上下文所有者。
  2. 默认情况下,SecurityContextRepository使用的是HttpSessionSecurityContextRepository作为实现类,有关于HttpSession的配置选项信息请查看HttpSessionSecurityContextRepository。
  3. 这个过滤器会在每次请求时都会调用,为的是解决servlet容器的兼容性(特别是Weblogic)。
  4. 这个过滤器必须在任何认证处理机制调用前执行,例如BASIC、CAS认证处理过滤器等都期望在它们执行时能从SecurityContextHolder中获取一个合法的SecurityContext。
  5. 这个过滤器实质上是对HttpSessionSecurityContextRepository进行了重构,以将存储问题委托给了单独的策略,从而允许在请求之间维护安全上下文的方式进行更多自定义。

首先看下SecurityContextPersistenceFilter的类结构: 在这里插入图片描述 首先它的父类GenericeFilterBean实现了很多接口,其中有三个XXAware的接口,表示的是具备注入XX到GenericFilterBean能力,而这一般都是通过setter来注入XX实现的。

而实现了DisposalbleBean表明了在注销bean时能进行额外的工作,实现InitializingBean表明了能在初始化时进行额外的工作。

源码分析

java
public class SecurityContextPersistenceFilter extends GenericFilterBean {

	static final String FILTER_APPLIED = "__spring_security_scpf_applied";

	private SecurityContextRepository repo;

	private boolean forceEagerSessionCreation = false;

	// 默认构造方法,传入HttpSessionSecurityContextRepository
	public SecurityContextPersistenceFilter() {
		this(new HttpSessionSecurityContextRepository());
	}

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

	// 过滤核心方法:doFilter
	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;
		
		// 判断请求中是否包含属性:__spring_security_scpf_applied,表示已经调用过SecurityContextPersistenceFilter了
		if (request.getAttribute(FILTER_APPLIED) != null) {
			// 确保每次请求只调用一次该过滤器
			chain.doFilter(request, response);
			return;
		}

		final boolean debug = logger.isDebugEnabled();
		// 为此次请求设置__spring_security_scpf_applied为true,防止下次同样请求再次调用进来时,重复执行以下逻辑
		request.setAttribute(FILTER_APPLIED, Boolean.TRUE);

		// 急切的想要创建Session
		if (forceEagerSessionCreation) {
			HttpSession session = request.getSession();

			if (debug && session.isNew()) {
				logger.debug("Eagerly created session: " + session.getId());
			}
		}
		
		HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
				response);
		// 从HttpSessionSecurityContextRepository中获取SecurityContext安全上下文
		SecurityContext contextBeforeChainExecution = repo.loadContext(holder);

		try {
			// 将获取的安全上下文存储到SecurityContextHolder中
			SecurityContextHolder.setContext(contextBeforeChainExecution);
			// 放过滤器链执行
			chain.doFilter(holder.getRequest(), holder.getResponse());

		}
		finally {
			// 等FilterChainProxy后面所有过滤器链都执行完毕时,进入finally块
			// 获取FilterChainProxy调用后的SecurityContext
			SecurityContext contextAfterChainExecution = SecurityContextHolder
					.getContext();
			// 清除SecurityContextHolder
			SecurityContextHolder.clearContext();
			// 将安全上下文存储到HttpSessionSecurityContextRepository中,也就是持久化到Session中
			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;
	}
}

从SecurityContextPersistenceFilter类的作用可以看出,它其实就是持久化SecurityContext。

ExceptionTranslationFilter

先看下ExceptionTranslationFilter的类注释,总结为以下几点:

  1. 此过滤器会处理任何AccessDeniedException和AuthenticationException的异常。
  2. 此过滤器是必要的,因为它提供了一个桥梁用于连接Java异常和HTTP响应。它仅和维护用户界面有关,而不会执行任何的安全性强制措施。
  3. 如果此过滤器捕获到了AuthenticationException,该Filter会加载AuthenticationEntrypoint。它允许处理任何从AbstractSecurityInterceptor子类抛出的authentication异常,AbstractSecurityInterceptor的子类即FilterChainProxy中包含的哪些过滤器。
  4. 如果捕获到了AccessDeniedException,此过滤器会判断当前用户是否是一个匿名用户。如果是匿名用户,则加载authenticationEntryPoint。如果不是匿名用户,则此过滤器会将逻辑代理到AccessDeniedHandler,由其处理接下来的逻辑。
  5. authenticationEntryPoint指示如果检测到AuthenticationException,则通过调用authenticationEntrypoint的commence方法开始认证过程的处理。需要注意的是,在ExceptionTranslationFilter中的requestCache用于保存身份验证过程中的认证结果,一边可以在用户认证通过后即可检索以及重用,requestCache的默认实现是HttpSessionRequestCache。

小结:ExceptionTranslationFilter的作用即捕获AuthenticationException和AccessDeniedException,并作出相应的处理;对于捕获AccessDeniedException时,如果是匿名用户则去调用authenticationEntryPoint去进行身份验证,如果不是匿名用户则直接抛出AccessDeniedException。

源码分析

先判断看下ExceptionTranslationFilter的成员变量

java
public class ExceptionTranslationFilter extends GenericFilterBean {
	
	// AccessDeniedException处理器
	private AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();
	// 用于进行身份验证的端点
	private AuthenticationEntryPoint authenticationEntryPoint;
	
	// 身份认证信任机制,包括判断是否是匿名,判断是否是RememberMe
	private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();
	// 异常分析器
	private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer();
	
	// 将身份认证结果存储在HttpSession中
	private RequestCache requestCache = new HttpSessionRequestCache();
	
	// 消息源转化器
	private final MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

	public ExceptionTranslationFilter(AuthenticationEntryPoint authenticationEntryPoint) {
		this(authenticationEntryPoint, new HttpSessionRequestCache());
	}

	public ExceptionTranslationFilter(AuthenticationEntryPoint authenticationEntryPoint,
			RequestCache requestCache) {
		Assert.notNull(authenticationEntryPoint,
				"authenticationEntryPoint cannot be null");
		Assert.notNull(requestCache, "requestCache cannot be null");
		this.authenticationEntryPoint = authenticationEntryPoint;
		this.requestCache = requestCache;
	}
	
	// 省略
}

doFilter源码分析

java
	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) {
			// 尝试去获取SpringSecurityException异常
			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) {
				if (response.isCommitted()) {
					throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex);
				}
				// 处理SpringSecurity异常
				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);
			}
		}
	}
java
	// SpringSecurityException异常处理的核心逻辑
	private void handleSpringSecurityException(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, RuntimeException exception)
			throws IOException, ServletException {
		// 如果是认证异常
		if (exception instanceof AuthenticationException) {
			logger.debug(
					"Authentication exception occurred; redirecting to authentication entry point",
					exception);
			// 开始进行身份认证
			sendStartAuthentication(request, response, chain,
					(AuthenticationException) exception);
		}
		else if (exception instanceof AccessDeniedException) {  // 如果是访问拒绝异常
			// 尝试从SecurityContextHolder缓存中获取认证结果
			Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
			// 判断认证结果是否是匿名的或者是rememberme
			if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
				logger.debug(
						"Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point",
						exception);
				// 开始进行身份认证
				sendStartAuthentication(
						request,
						response,
						chain,
						new InsufficientAuthenticationException(
							messages.getMessage(
								"ExceptionTranslationFilter.insufficientAuthentication",
								"Full authentication is required to access this resource")));
			}
			else {
				
				logger.debug(
						"Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
						exception);
				// 如果既不是匿名用户也不是rememberme用户,则调用访问拒绝处理器			
				accessDeniedHandler.handle(request, response,
						(AccessDeniedException) exception);
			}
		}
	}
java
	protected void sendStartAuthentication(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain,
			AuthenticationException reason) throws ServletException, IOException {
		// 清空缓存中的认证结果,重新进行身份验证
		SecurityContextHolder.getContext().setAuthentication(null);
		// 将认证请求request和响应response存储在session中
		requestCache.saveRequest(request, response);
		logger.debug("Calling Authentication entry point.");
		// 进行身份验证
		authenticationEntryPoint.commence(request, response, reason);
	}

先看下commence的实现类: 在这里插入图片描述

这里这么多实现类,到底调用哪一个呢?这就要看下authenticationEntryPoint注入的什么实现类了,可以将断点打在ExceptionTranslationFilter的构造方法中。 在这里插入图片描述

启动项目之后,进入方法调用栈,可以在图中位置看到在进行安全配置类配置时,会调用ExceptionHandlingConfigurer这个配置类的configure方法。 在这里插入图片描述

ExceptionHandlingConfigurer

进入其configure方法查看

java
	@Override
	public void configure(H http) throws Exception {
		// 获取authenticationEntryPoint
		AuthenticationEntryPoint entryPoint = getAuthenticationEntryPoint(http);
		// 新建一个ExceptionTranslationFilter对象
		ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter(
				entryPoint, getRequestCache(http));
		// 或获取访问拒绝处理器
		AccessDeniedHandler deniedHandler = getAccessDeniedHandler(http);
		exceptionTranslationFilter.setAccessDeniedHandler(deniedHandler);
		exceptionTranslationFilter = postProcess(exceptionTranslationFilter);
		// 往FilterChainProxy中添加ExceptionTranslationFilter
		http.addFilter(exceptionTranslationFilter);
	}

在这里插入图片描述 可以发现在实例化完ExceptionHandlingConfigurer后,依然没有注入authenticationEntryPoint。所以是在调用configure方法时,去调用getAuthenticationEntryPoint()去获取authenticationEntryPoint。

下面接着查看一下getAuthenticationEntryPoint()方法

java
	AuthenticationEntryPoint getAuthenticationEntryPoint(H http) {
		AuthenticationEntryPoint entryPoint = this.authenticationEntryPoint;
		// 由于entryPoint为空,所以调用createDefaultEntryPoint去创建entryPoint
		if (entryPoint == null) {
			entryPoint = createDefaultEntryPoint(http);
		}
		return entryPoint;
	}
java
	private AuthenticationEntryPoint createDefaultEntryPoint(H http) {
		// 如果entryPointMappings为空,则返回Http403ForbiddenEntryPoint
		if (this.defaultEntryPointMappings.isEmpty()) {
			return new Http403ForbiddenEntryPoint();
		}
		if (this.defaultEntryPointMappings.size() == 1) {
			// 遍历defaultEntryPointMappings,获取其中存储的entrypoint
			return this.defaultEntryPointMappings.values().iterator().next();
		}
		// 创建DelegatingAuthenticationEntryPoint这个代理类
		DelegatingAuthenticationEntryPoint entryPoint = new DelegatingAuthenticationEntryPoint(
				this.defaultEntryPointMappings);
		entryPoint.setDefaultEntryPoint(this.defaultEntryPointMappings.values().iterator()
				.next());
		return entryPoint;
	}

可以看出,最终返回的就是:Http403ForbiddenEntryPoint 在这里插入图片描述

可以看到,HTTP403ForbiddenEntryPiont这个类代码非常少

java
public class Http403ForbiddenEntryPoint implements AuthenticationEntryPoint {
	private static final Log logger = LogFactory.getLog(Http403ForbiddenEntryPoint.class);

	/**
	 * Always returns a 403 error code to the client.
	 */
	public void commence(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException arg2) throws IOException, ServletException {
		if (logger.isDebugEnabled()) {
			logger.debug("Pre-authenticated entry point called. Rejecting access");
		}
		// 在response响应中添加403 Forbidden,访问拒绝异常
		response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied");
	}
}

这里,还讲解一下另外一个类LoginURLAuthenticationEntryPoint的方法commence。

java
	public void commence(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException authException) throws IOException, ServletException {
		// 重定向url
		String redirectUrl = null;

		if (useForward) {
			// 判断下请求协议是否是http
			if (forceHttps && "http".equals(request.getScheme())) {
				// 获取重定向完整的URL路径
				redirectUrl = buildHttpsRedirectUrlForRequest(request);
			}

			if (redirectUrl == null) {
				// 如果重定向地址为空,则获取默认的登录form表单地址;用户可以自定义设置;
				String loginForm = determineUrlToUseForThisRequest(request, response,
						authException);

				if (logger.isDebugEnabled()) {
					logger.debug("Server side forward to: " + loginForm);
				}

				RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);

				dispatcher.forward(request, response);

				return;
			}
		}
		else {
			redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);

		}
		// 发送重定向请求
		redirectStrategy.sendRedirect(request, response, redirectUrl);
	}

LoginURLAuthenticationEntryPoint这个类其实就是重定向到login页面,如果用户不指定login页面,则重定向到默认的login页面。

FilterSecurityInterceptor

FilterSecurityInterceptor 是 Spring Security 中一个核心的过滤器,负责对请求进行访问控制和权限验证。它是 FilterChainProxy 中的最后一个过滤器,主要通过 AccessDecisionManager 进行权限校验。以下是其类注释内容的总结:

  • 负责保护 HTTP 请求资源。
  • 通过 SecurityMetadataSource 获取当前请求对应的权限配置。
  • 利用 Authentication 和 AccessDecisionManager 来判断当前用户是否有权访问资源。
  • 如果权限不足,抛出 AccessDeniedException 或 AuthenticationException。
  • 提供了一个默认实现类,可以通过自定义扩展 SecurityMetadataSource 和 AccessDecisionManager 来实现定制化。 FilterSecurityInterceptor 的作用可以简单概括为:根据用户身份认证信息和资源的权限配置,判断用户是否有权访问资源,并在无权限时抛出相应的异常。

类结构分析

FilterSecurityInterceptor 继承了 AbstractSecurityInterceptor,并实现了 Filter 接口:

  • AbstractSecurityInterceptor: 提供了核心的安全访问控制逻辑。
  • Filter: 实现了过滤器接口,能拦截 HTTP 请求。

源码分析

FilterSecurityInterceptor 的核心逻辑体现在其 doFilter 方法中:

java
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {

    private FilterInvocationSecurityMetadataSource securityMetadataSource;

    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 void setSecurityMetadataSource(FilterInvocationSecurityMetadataSource newSource) {
        this.securityMetadataSource = newSource;
    }

    public Class<?> getSecureObjectClass() {
        return FilterInvocation.class;
    }

    @Override
    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return this.securityMetadataSource;
    }

    public void invoke(FilterInvocation fi) throws IOException, ServletException {
        // 核心方法:进行权限验证
        if (fi.getRequest() != null && fi.getRequest().getAttribute(FILTER_APPLIED) != null) {
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
            return;
        }

        fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);

        // 调用 AbstractSecurityInterceptor 的权限验证逻辑
        InterceptorStatusToken token = super.beforeInvocation(fi);

        try {
            // 放行过滤器链,继续调用后续过滤器
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } finally {
            super.afterInvocation(token, null);
        }
    }
}

核心方法解析

invoke(FilterInvocation fi):

  • 通过 FilterInvocation 包装当前请求。
  • 调用父类的 beforeInvocation 方法进行权限验证。
  • 验证通过后调用 doFilter 放行。
  • 最后调用 afterInvocation 方法清理上下文。

beforeInvocation:

  • 获取 SecurityMetadataSource 中的权限配置。
  • 通过 AccessDecisionManager 判断用户是否有访问权限。
  • 如果权限不足,抛出 AccessDeniedException。

afterInvocation:

  • 在请求完成后进行收尾工作,如清理权限信息或记录日志。

配置分析

FilterSecurityInterceptor 的行为依赖于以下几个组件:

SecurityMetadataSource: 定义资源和权限的映射关系。

通常由 DefaultFilterInvocationSecurityMetadataSource 实现,通过配置文件或注解动态加载。

AccessDecisionManager: 决定用户是否有权访问资源。

默认实现有:

  • AffirmativeBased: 只要一个 AccessDecisionVoter 通过即可。
  • ConsensusBased: 根据投票数判断。
  • UnanimousBased: 所有 AccessDecisionVoter 都通过。

AuthenticationManager: 提供用户身份认证的功能。

工作流程

  • 请求进入 FilterSecurityInterceptor: 封装为 FilterInvocation 对象。
  • 加载权限配置: 调用 SecurityMetadataSource 获取资源所需权限。
  • 权限判断: 调用 AccessDecisionManager 和 AccessDecisionVoter 判断用户是否有权限访问资源。
  • 请求放行或拦截: 如果有权限,调用 FilterChain.doFilter 放行。如果无权限,抛出异常。

总结

  • FilterSecurityInterceptor 是实际执行权限验证的拦截器,通过 SecurityMetadataSource 获取请求所需的权限,并使用 AccessDecisionManager 判断是否放行请求。
  • AccessDecisionManager 是决策管理器,负责评估用户是否有权访问资源,基于投票机制进行权限判断。
  • SecurityMetadataSource 提供权限数据源,定义了哪些资源需要哪些权限,从而帮助权限决策。

这三者协同工作,提供了 Spring Security 中强大的访问控制能力。

粤ICP备20009776号