架构
过滤器回顾
Spring Security 的 Servlet 支持基于 Servlet 过滤器,因此首先了解过滤器的通用角色会很有帮助。下图展示了对单个 HTTP 请求处理器的典型分层结构。

图 1. FilterChain
客户端向应用程序发送请求,容器会根据请求URI的路径创建一个FilterChain,其中包含应处理HttpServletRequest的Filter实例和Servlet。在Spring MVC应用程序中,该Servlet是DispatcherServlet的一个实例。最多只能有一个Servlet处理单个HttpServletRequest和HttpServletResponse。然而,可以使用多个Filter来:
-
阻止下游的
Filter实例或Servlet被调用。在这种情况下,Filter通常会写入HttpServletResponse。 -
修改下游
Filter实例和Servlet所使用的HttpServletRequest或HttpServletResponse。
Filter 的强大之处在于传入其中的 FilterChain。
- Java
- Kotlin
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
// do something before the rest of the application
chain.doFilter(request, response); // invoke the rest of the application
// do something after the rest of the application
}
@Throws(IOException::class, ServletException::class)
override fun doFilter(request: ServletRequest?, response: ServletResponse?, chain: FilterChain) {
// do something before the rest of the application
chain.doFilter(request, response) // invoke the rest of the application
// do something after the rest of the application
}
由于Filter仅影响下游的Filter实例和Servlet,因此每个Filter的调用顺序至关重要。
DelegatingFilterProxy
Spring 提供了一个名为 DelegatingFilterProxy 的 Filter 实现,它允许在 Servlet 容器的生命周期与 Spring 的 ApplicationContext 之间建立桥梁。Servlet 容器允许使用其自身标准注册 Filter 实例,但它并不感知 Spring 定义的 Bean。您可以通过标准的 Servlet 容器机制注册 DelegatingFilterProxy,但将所有工作委托给一个实现了 Filter 接口的 Spring Bean。
这是一张展示 DelegatingFilterProxy 如何融入 Filter 实例与 FilterChain 的示意图。

图 2. DelegatingFilterProxy
DelegatingFilterProxy 从 ApplicationContext 中查找 Bean Filter0,然后调用 Bean Filter0。以下清单展示了 DelegatingFilterProxy 的伪代码:
- Java
- Kotlin
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
Filter delegate = getFilterBean(someBeanName); 1
delegate.doFilter(request, response); 2
}
fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
val delegate: Filter = getFilterBean(someBeanName) 1
delegate.doFilter(request, response) 2
}
延迟获取已注册为 Spring Bean 的 Filter。对于 DelegatingFilterProxy 中的示例,
delegate是 Bean Filter0 的一个实例。将工作委托给 Spring Bean。
DelegatingFilterProxy 的另一个优势在于它允许延迟查找 Filter bean 实例。这一点非常重要,因为容器需要在启动之前注册 Filter 实例。然而,Spring 通常使用 ContextLoaderListener 来加载 Spring Bean,而这一过程直到 Filter 实例需要被注册之后才会完成。
FilterChainProxy
Spring Security 的 Servlet 支持包含在 FilterChainProxy 中。FilterChainProxy 是 Spring Security 提供的一个特殊的 Filter,它允许通过 SecurityFilterChain 委托给多个 Filter 实例。由于 FilterChainProxy 是一个 Bean,它通常被包装在一个 DelegatingFilterProxy 中。
下图展示了 FilterChainProxy 的作用。

图 3. FilterChainProxy
SecurityFilterChain
SecurityFilterChain 被 FilterChainProxy 用于确定当前请求应调用哪些 Spring Security Filter 实例。
下图展示了 SecurityFilterChain 的作用。

图 4. SecurityFilterChain
安全过滤器在 SecurityFilterChain 中通常是 Bean,但它们是通过 FilterChainProxy 注册的,而不是通过 DelegatingFilterProxy。与直接注册到 Servlet 容器或 DelegatingFilterProxy 相比,FilterChainProxy 提供了诸多优势。首先,它为 Spring Security 的所有 Servlet 支持提供了一个统一的入口点。因此,如果你需要排查 Spring Security 的 Servlet 支持相关问题,在 FilterChainProxy 中添加调试断点是一个很好的切入点。
其次,由于 FilterChainProxy 是 Spring Security 使用的核心组件,它可以执行一些不被视为可选的任务。例如,它会清除 SecurityContext 以避免内存泄漏。同时,它还会应用 Spring Security 的 HttpFirewall 来保护应用程序免受某些类型的攻击。
此外,它在确定何时调用 SecurityFilterChain 方面提供了更大的灵活性。在 Servlet 容器中,Filter 实例仅基于 URL 被调用。然而,FilterChainProxy 可以通过使用 RequestMatcher 接口,基于 HttpServletRequest 中的任何内容来确定调用时机。
下图展示了多个 SecurityFilterChain 实例:

图 5. 多个 SecurityFilterChain
在多个SecurityFilterChain图示中,FilterChainProxy 负责决定应使用哪个 SecurityFilterChain。只有第一个匹配的 SecurityFilterChain 会被调用。如果请求的 URL 是 /api/messages/,它会首先匹配 SecurityFilterChain0 的模式 /api/**,因此即使它也匹配 SecurityFilterChainn,也只会调用 SecurityFilterChain0。如果请求的 URL 是 /messages/,它不匹配 SecurityFilterChain0 的模式 /api/**,因此 FilterChainProxy 会继续尝试每个 SecurityFilterChain。假设没有其他 SecurityFilterChain 实例匹配,则会调用 SecurityFilterChainn。
请注意,SecurityFilterChain0 仅配置了三个安全 Filter 实例。然而,SecurityFilterChainn 配置了四个安全 Filter 实例。需要特别注意的是,每个 SecurityFilterChain 都可以是唯一的,并且可以独立配置。实际上,如果应用程序希望 Spring Security 忽略某些请求,SecurityFilterChain 甚至可能包含零个安全 Filter 实例。
安全过滤器
安全过滤器通过 SecurityFilterChain API 被插入到 FilterChainProxy 中。这些过滤器可用于多种不同的目的,例如漏洞防护、身份验证、授权等。过滤器按特定顺序执行,以确保它们在正确的时机被调用,例如,执行身份验证的 Filter 应在执行授权的 Filter 之前被调用。通常不需要了解 Spring Security Filter 的排序。然而,有时了解排序是有益的,如果你想知道它们,可以查看 FilterOrderRegistration 代码。
这些安全过滤器最常通过 HttpSecurity 实例来声明。为了举例说明上述段落,让我们考虑以下安全配置:
- Java
- Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults())
.formLogin(Customizer.withDefaults())
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
);
return http.build();
}
}
import org.springframework.security.config.web.servlet.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
csrf { }
httpBasic { }
formLogin { }
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
}
return http.build()
}
}
上述配置将产生以下 Filter 排序:
| 过滤器 | 添加方式 |
|---|---|
| CsrfFilter | HttpSecurity#csrf |
| BasicAuthenticationFilter | HttpSecurity#httpBasic |
| UsernamePasswordAuthenticationFilter | HttpSecurity#formLogin |
| AuthorizationFilter | HttpSecurity#authorizeHttpRequests |
-
首先,调用
CsrfFilter以防止 CSRF 攻击。 -
其次,调用 身份验证过滤器 对请求进行身份验证。
-
最后,调用 AuthorizationFilter 对请求进行授权。
可能还有其他未列出的 Filter 实例。如果您想查看特定请求所调用的过滤器列表,可以打印它们。
打印安全过滤器
通常,查看针对特定请求调用的安全 Filter 列表是很有用的。例如,你可能需要确认你添加的过滤器是否在安全过滤器列表中。
过滤器列表会在应用程序启动时以 DEBUG 级别打印,因此您可以在控制台输出中看到类似以下内容:
2023-06-14T08:55:22.321-03:00 DEBUG 76975 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [ DisableEncodeUrlFilter, WebAsyncManagerIntegrationFilter, SecurityContextHolderFilter, HeaderWriterFilter, CsrfFilter, LogoutFilter, UsernamePasswordAuthenticationFilter, DefaultLoginPageGeneratingFilter, DefaultLogoutPageGeneratingFilter, BasicAuthenticationFilter, RequestCacheAwareFilter, SecurityContextHolderAwareRequestFilter, AnonymousAuthenticationFilter, ExceptionTranslationFilter, AuthorizationFilter]
这将让你对每个过滤器链配置的安全过滤器有一个很好的了解。
但这还不是全部,你还可以配置应用程序,使其为每个请求打印每个独立过滤器的调用情况。这有助于查看你添加的过滤器是否针对特定请求被调用,或者检查异常来自何处。为此,你可以配置应用程序以记录安全事件。
向过滤器链添加过滤器
大多数情况下,默认的安全过滤器足以保障应用程序的安全。然而,有时您可能需要在SecurityFilterChain中添加自定义的Filter。
HttpSecurity 提供了三种方法来添加过滤器:
-
#addFilterBefore(Filter, Class<?>)在另一个过滤器之前添加你的过滤器 -
#addFilterAfter(Filter, Class<?>)在另一个过滤器之后添加你的过滤器 -
#addFilterAt(Filter, Class<?>)用你的过滤器替换另一个过滤器
添加自定义过滤器
如果您正在创建自己的过滤器,您需要确定它在过滤器链中的位置。请查看以下过滤器链中发生的关键事件:
考虑需要发生哪些事件才能定位你的过滤器。以下是一个经验法则:
| 如果您的过滤器是 | 则应将其置于 | 因为这些事件已经发生 |
|---|---|---|
| 漏洞利用防护过滤器 | SecurityContextHolderFilter | 1 |
| 身份验证过滤器 | LogoutFilter | 1, 2 |
| 授权过滤器 | AnonymousAuthenticationFilter | 1, 2, 3 |
通常情况下,应用程序会添加自定义的身份验证。这意味着它们应该放置在 LogoutFilter 之后。
例如,假设您想要添加一个Filter,用于获取租户ID头部信息,并检查当前用户是否有权访问该租户。
首先,我们来创建 Filter:
import java.io.IOException;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
public class TenantFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String tenantId = request.getHeader("X-Tenant-Id"); 1
boolean hasAccess = isUserAllowed(tenantId); 2
if (hasAccess) {
filterChain.doFilter(request, response); 3
return;
}
throw new AccessDeniedException("Access denied"); 4
}
}
上述示例代码执行了以下操作:
从请求头中获取租户ID。
检查当前用户是否有权访问该租户ID。
如果用户有权访问,则调用过滤器链中的其余过滤器。
如果用户无权访问,则抛出
AccessDeniedException。
除了实现 Filter 接口,你也可以继承 OncePerRequestFilter。这是一个过滤器基类,确保每个请求仅被调用一次,并提供了一个带有 HttpServletRequest 和 HttpServletResponse 参数的 doFilterInternal 方法。
现在,你需要将过滤器添加到 SecurityFilterChain 中。之前的描述已经为我们提供了添加过滤器位置的线索,由于我们需要知道当前用户,因此需要在身份验证过滤器之后添加它。
根据经验法则,将其添加在AnonymousAuthenticationFilter之后,即过滤器链中的最后一个认证过滤器,如下所示:
- Java
- Kotlin
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// ...
.addFilterAfter(new TenantFilter(), AnonymousAuthenticationFilter.class); 1
return http.build();
}
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http
// ...
.addFilterAfter(TenantFilter(), AnonymousAuthenticationFilter::class.java) 1
return http.build()
}
使用
HttpSecurity#addFilterAfter将TenantFilter添加到AnonymousAuthenticationFilter之后。
通过在AnonymousAuthenticationFilter之后添加过滤器,我们确保TenantFilter在身份验证过滤器之后被调用。
就是这样,现在 TenantFilter 将在过滤器链中被调用,并检查当前用户是否有权访问该租户 ID。
将过滤器声明为 Bean
当你将 Filter 声明为 Spring bean 时,无论是通过 @Component 注解还是通过在配置中将其声明为 bean,Spring Boot 都会自动将其注册到嵌入式容器中。这可能导致过滤器被调用两次,一次由容器调用,另一次由 Spring Security 调用,并且调用顺序也可能不同。
因此,过滤器通常不是 Spring bean。
然而,如果你的过滤器需要成为Spring Bean(例如,为了利用依赖注入),你可以通过声明一个FilterRegistrationBean Bean并将其enabled属性设置为false,来告知Spring Boot不要将其注册到容器中:
@Bean
public FilterRegistrationBean<TenantFilter> tenantFilterRegistration(TenantFilter filter) {
FilterRegistrationBean<TenantFilter> registration = new FilterRegistrationBean<>(filter);
registration.setEnabled(false);
return registration;
}
这样一来,只有 HttpSecurity 会添加它。
自定义 Spring Security 过滤器
通常,你可以使用过滤器的DSL方法来配置Spring Security的过滤器。例如,添加BasicAuthenticationFilter的最简单方式是让DSL来完成:
- Java
- Kotlin
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.httpBasic(Customizer.withDefaults())
// ...
return http.build();
}
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
httpBasic { }
// ...
}
return http.build()
}
然而,如果您想自行构建一个 Spring Security 过滤器,可以在 DSL 中使用 addFilterAt 来指定它,如下所示:
- Java
- Kotlin
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
BasicAuthenticationFilter basic = new BasicAuthenticationFilter();
// ... configure
http
// ...
.addFilterAt(basic, BasicAuthenticationFilter.class);
return http.build();
}
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
val basic = BasicAuthenticationFilter()
// ... configure
http
// ...
.addFilterAt(basic, BasicAuthenticationFilter::class.java)
return http.build()
}
请注意,如果该过滤器已被添加,Spring Security 将抛出异常。例如,调用 HttpSecurity#httpBasic 会自动添加一个 BasicAuthenticationFilter。因此,以下配置会失败,因为两次调用都试图添加 BasicAuthenticationFilter:
- Java
- Kotlin
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
BasicAuthenticationFilter basic = new BasicAuthenticationFilter();
// ... configure
http
.httpBasic(Customizer.withDefaults())
// ... on no! BasicAuthenticationFilter is added twice!
.addFilterAt(basic, BasicAuthenticationFilter.class);
return http.build();
}
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
val basic = BasicAuthenticationFilter()
// ... configure
http {
httpBasic { }
}
// ... on no! BasicAuthenticationFilter is added twice!
http.addFilterAt(basic, BasicAuthenticationFilter::class.java)
return http.build()
}
在这种情况下,请移除对 httpBasic 的调用,因为您正在自行构建 BasicAuthenticationFilter。
如果你无法重新配置 HttpSecurity 以不添加某个过滤器,通常可以通过调用其 DSL 的 disable 方法来禁用 Spring Security 过滤器,如下所示:
.httpBasic((basic) -> basic.disable())
处理安全异常
ExceptionTranslationFilter 允许将 AccessDeniedException 和 AuthenticationException 转换为 HTTP 响应。
ExceptionTranslationFilter 作为安全过滤器之一,被插入到FilterChainProxy中。
下图展示了 ExceptionTranslationFilter 与其他组件的关系:

-
1 首先,
ExceptionTranslationFilter调用FilterChain.doFilter(request, response)来执行应用程序的其余部分。 -
2 如果用户未认证或发生
AuthenticationException,则开始认证。- 清空 SecurityContextHolder。
- 保存
HttpServletRequest(已保存的请求),以便在认证成功后重放原始请求。 - 使用
AuthenticationEntryPoint向客户端请求凭据。例如,它可能重定向到登录页面或发送WWW-Authenticate头。
-
3 否则,如果是
AccessDeniedException,则访问被拒绝。调用AccessDeniedHandler来处理访问被拒绝的情况。
如果应用程序没有抛出 AccessDeniedException 或 AuthenticationException,那么 ExceptionTranslationFilter 不会执行任何操作。
ExceptionTranslationFilter 的伪代码大致如下:
try {
filterChain.doFilter(request, response); 1
} catch (AccessDeniedException | AuthenticationException ex) {
if (!authenticated || ex instanceof AuthenticationException) {
startAuthentication(); 2
} else {
accessDenied(); 3
}
}
如 过滤器综述 所述,调用
FilterChain.doFilter(request, response)等同于调用应用程序的其余部分。这意味着如果应用程序的另一部分(AuthorizationFilter 或方法安全)抛出AuthenticationException或AccessDeniedException,它会被捕获并在这里处理。如果用户未通过身份验证或这是一个
AuthenticationException,则开始身份验证。否则,拒绝访问
保存认证之间的请求
如处理安全异常所述,当请求未携带认证信息且访问的是需要认证的资源时,需要保存该请求以便在认证成功后重新发起。在 Spring Security 中,这是通过使用 RequestCache 实现来保存 HttpServletRequest 完成的。
RequestCache
HttpServletRequest 被保存在 RequestCache 中。当用户成功认证后,RequestCache 会被用来重放原始请求。RequestCacheAwareFilter 在用户认证后使用 RequestCache 来获取保存的 HttpServletRequest,而 ExceptionTranslationFilter 则在检测到 AuthenticationException 后、重定向用户到登录端点之前,使用 RequestCache 来保存 HttpServletRequest。
默认情况下,系统使用 HttpSessionRequestCache。以下代码演示了如何自定义 RequestCache 实现,该实现用于在存在名为 continue 的参数时检查 HttpSession 中是否有已保存的请求。
- Java
- Kotlin
- XML
@Bean
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
requestCache.setMatchingRequestParameterName("continue");
http
// ...
.requestCache((cache) -> cache
.requestCache(requestCache)
);
return http.build();
}
@Bean
open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
val httpRequestCache = HttpSessionRequestCache()
httpRequestCache.setMatchingRequestParameterName("continue")
http {
requestCache {
requestCache = httpRequestCache
}
}
return http.build()
}
<http auto-config="true">
<!-- ... -->
<request-cache ref="requestCache"/>
</http>
<b:bean id="requestCache" class="org.springframework.security.web.savedrequest.HttpSessionRequestCache"
p:matchingRequestParameterName="continue"/>
防止请求被保存
您可能出于多种原因不希望将用户的未认证请求存储在会话中。您可能希望将该存储卸载到用户的浏览器上,或将其存储在数据库中。或者,您可能希望关闭此功能,因为您总是希望将用户重定向到主页,而不是他们登录前尝试访问的页面。
为此,你可以使用 NullRequestCache 实现。
- Java
- Kotlin
- XML
@Bean
SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
RequestCache nullRequestCache = new NullRequestCache();
http
// ...
.requestCache((cache) -> cache
.requestCache(nullRequestCache)
);
return http.build();
}
@Bean
open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
val nullRequestCache = NullRequestCache()
http {
requestCache {
requestCache = nullRequestCache
}
}
return http.build()
}
<http auto-config="true">
<!-- ... -->
<request-cache ref="nullRequestCache"/>
</http>
<b:bean id="nullRequestCache" class="org.springframework.security.web.savedrequest.NullRequestCache"/>
RequestCacheAwareFilter
RequestCacheAwareFilter 使用 RequestCache 来重放原始请求。
日志记录
Spring Security 在 DEBUG 和 TRACE 级别提供了所有安全相关事件的全面日志记录。这在调试应用程序时非常有用,因为出于安全考虑,Spring Security 不会在响应体中添加任何关于请求被拒绝原因的详细信息。如果您遇到 401 或 403 错误,很可能您会找到一条日志消息,帮助您了解发生了什么。
让我们考虑一个例子:用户尝试向一个启用了CSRF保护的资源发起 POST 请求,但没有提供 CSRF 令牌。如果没有日志记录,用户只会看到一个 403 错误,而无法得知请求被拒绝的原因。然而,如果你为 Spring Security 启用了日志记录,就会看到类似这样的日志信息:
2023-06-14T09:44:25.797-03:00 DEBUG 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Securing POST /hello
2023-06-14T09:44:25.797-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking DisableEncodeUrlFilter (1/15)
2023-06-14T09:44:25.798-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking WebAsyncManagerIntegrationFilter (2/15)
2023-06-14T09:44:25.800-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking SecurityContextHolderFilter (3/15)
2023-06-14T09:44:25.801-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking HeaderWriterFilter (4/15)
2023-06-14T09:44:25.802-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking CsrfFilter (5/15)
2023-06-14T09:44:25.814-03:00 DEBUG 76975 --- [nio-8080-exec-1] o.s.security.web.csrf.CsrfFilter : Invalid CSRF token found for http://localhost:8080/hello
2023-06-14T09:44:25.814-03:00 DEBUG 76975 --- [nio-8080-exec-1] o.s.s.w.access.AccessDeniedHandlerImpl : Responding with 403 status code
2023-06-14T09:44:25.814-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.s.w.header.writers.HstsHeaderWriter : Not injecting HSTS header since it did not match request to [Is Secure]
很明显,CSRF 令牌缺失,这就是请求被拒绝的原因。
要将您的应用程序配置为记录所有安全事件,您可以在应用程序中添加以下内容:
logging.level.org.springframework.security=TRACE
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- ... -->
</appender>
<!-- ... -->
<logger name="org.springframework.security" level="trace" additivity="false">
<appender-ref ref="Console" />
</logger>
</configuration>