架构
Filter 回顾
Spring Security 的 Servlet 支持基于 Servlet Filter,因此首先了解 Filter 的一般作用会很有帮助。下图展示了单个 HTTP 请求的处理器典型分层。

客户端向应用发送请求,容器创建一个 FilterChain
,其中包含根据请求 URI 的路径应处理 HttpServletRequest
的 Filter
实例和 Servlet
。在 Spring MVC 应用中,该 Servlet
是 DispatcherServlet
的一个实例。最多一个 Servlet
可以处理单个 HttpServletRequest
和 HttpServletResponse
。然而,可以使用不止一个 Filter
来
-
阻止下游
Filter
实例或Servlet
的调用。在这种情况下,Filter
通常负责写入HttpServletResponse
。 -
修改下游
Filter
实例和Servlet
使用的HttpServletRequest
或HttpServletResponse
。
Filter
的强大之处在于传递给它的 FilterChain
。
FilterChain
使用示例-
Java
-
Kotlin
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
// 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
}
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
。

DelegatingFilterProxy
从 ApplicationContext
中查找 Bean Filter0,然后调用 Bean Filter0。下面的列表展示了 DelegatingFilterProxy
的伪代码
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)
}
1 | 延迟获取注册为 Spring Bean 的 Filter。对于DelegatingFilterProxy 中的示例,delegate 是 Bean Filter0 的一个实例。 |
2 | 将工作委托给 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
的作用。

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

SecurityFilterChain
中的安全 Filter 通常是 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
实例

在多个 SecurityFilterChain 图中,FilterChainProxy
决定应该使用哪个 SecurityFilterChain
。只有第一个匹配的 SecurityFilterChain
会被调用。如果请求的 URL 是 /api/messages/
,它首先匹配 SecurityFilterChain0
的模式 /api/**
,因此只调用 SecurityFilterChain0
,即使它也匹配 SecurityFilterChainn
。如果请求的 URL 是 /messages/
,它不匹配 SecurityFilterChain0
的模式 /api/**
,因此 FilterChainProxy
会继续尝试每个 SecurityFilterChain
。假设没有其他 SecurityFilterChain
实例匹配,则会调用 SecurityFilterChainn
。
请注意,SecurityFilterChain0
只配置了三个安全 Filter
实例。然而,SecurityFilterChainn
配置了四个安全 Filter
实例。重要的是要认识到每个 SecurityFilterChain
都可以是唯一的,并且可以独立配置。事实上,如果应用程序希望 Spring Security 忽略某些请求,SecurityFilterChain
可能包含零个安全 Filter
实例。
安全 Filter
安全 Filter 通过 SecurityFilterChain API 插入到 FilterChainProxy 中。这些 Filter 可用于多种不同目的,例如漏洞保护、认证、授权等。这些 Filter 以特定顺序执行,以确保它们在正确的时间被调用,例如,执行认证的 Filter
应在执行授权的 Filter
之前被调用。通常不需要知道 Spring Security 的 Filter
顺序。然而,有时了解顺序会有帮助,如果您想了解它们,可以查看 FilterOrderRegistration
代码。
这些安全 Filter 最常使用 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
顺序:
Filter | 添加者 |
---|---|
|
|
|
|
|
|
|
-
首先,调用
CsrfFilter
以防御CSRF 攻击。 -
其次,调用认证 Filter 对请求进行认证。
-
第三,调用
AuthorizationFilter
对请求进行授权。
可能还有上面未列出的其他 |
打印安全 Filter
通常,查看特定请求调用的安全 Filter
列表很有用。例如,您想确保您添加的 Filter 位于安全 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]
这将很好地了解为每个 Filter 链配置的安全 Filter。
但这还不是全部,您还可以配置应用程序为每个请求打印每个独立 Filter 的调用情况。这有助于查看您添加的 Filter 是否被特定请求调用,或者检查异常来自哪里。要做到这一点,您可以配置应用程序记录安全事件。
向 Filter 链添加 Filter
大多数情况下,默认的安全 Filter 足以为您的应用程序提供安全保障。然而,有时您可能希望向 SecurityFilterChain 添加自定义 Filter
。
HttpSecurity
提供了三种添加 Filter 的方法:
-
#addFilterBefore(Filter, Class<?>)
在另一个 Filter 之前添加您的 Filter -
#addFilterAfter(Filter, Class<?>)
在另一个 Filter 之后添加您的 Filter -
#addFilterAt(Filter, Class<?>)
用您的 Filter 替换另一个 Filter
添加自定义 Filter
如果您正在创建自己的 Filter,您需要确定它在 Filter 链中的位置。请查看 Filter 链中发生的以下关键事件:
考虑您的 Filter 需要哪些事件已经发生才能确定其位置。以下是一条经验法则:
如果您的 Filter 是 | 那么将其放在之后 | 因为这些事件已经发生 |
---|---|---|
漏洞保护 Filter |
SecurityContextHolderFilter |
1 |
认证 Filter |
LogoutFilter |
1, 2 |
授权 Filter |
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)
}
}
上面的示例代码执行以下操作:
1 | 从请求头部获取租户 ID。 |
2 | 检查当前用户是否具有访问该租户 ID 的权限。 |
3 | 如果用户具有权限,则调用链中的其余 Filter。 |
4 | 如果用户没有权限,则抛出 AccessDeniedException 。 |
除了实现 |
现在,您需要将 Filter 添加到 SecurityFilterChain 中。前面的描述已经提供了关于在哪里添加 Filter 的线索,因为我们需要知道当前用户,所以需要将其添加到认证 Filter 之后。
根据经验法则,将其添加到链中最后一个认证 Filter 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()
}
1 | 使用 HttpSecurity#addFilterAfter 将 TenantFilter 添加到 AnonymousAuthenticationFilter 之后。 |
通过将 Filter 添加到 AnonymousAuthenticationFilter
之后,我们确保 TenantFilter
在认证 Filter 之后被调用。
就是这样,现在 TenantFilter
将在 Filter 链中被调用,并检查当前用户是否具有访问该租户 ID 的权限。
将您的 Filter 声明为 Bean
当您将 Filter
声明为 Spring Bean 时,无论是通过使用 @Component
注解还是在您的配置中将其声明为 Bean,Spring Boot 都会自动将其注册到嵌入式容器。这可能导致 Filter 被调用两次,一次由容器调用,一次由 Spring Security 调用,并且顺序不同。
因此,Filter 通常不是 Spring Bean。
但是,如果您的 Filter 需要成为 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 Filter
通常,您可以使用 Filter 的 DSL 方法来配置 Spring Security 的 Filter。例如,添加 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 Filter,您可以使用 addFilterAt
在 DSL 中指定它,如下所示:
-
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()
}
注意,如果该 Filter 已经被添加,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()
}
在这种情况下,由于您自己构建 BasicAuthenticationFilter
,请移除对 httpBasic
的调用。
如果您无法重新配置
|
处理安全异常
ExceptionTranslationFilter
允许将 AccessDeniedException
和 AuthenticationException
转换为 HTTP 响应。
ExceptionTranslationFilter
作为安全 Filter 之一插入到 FilterChainProxy 中。
下图展示了 ExceptionTranslationFilter
与其他组件的关系:

-
首先,
ExceptionTranslationFilter
调用FilterChain.doFilter(request, response)
来调用应用程序的其余部分。 -
如果用户未认证或发生
AuthenticationException
,则*开始认证*。-
HttpServletRequest
被保存,以便在认证成功后用于重放原始请求。 -
使用
AuthenticationEntryPoint
从客户端请求凭据。例如,它可能重定向到登录页面或发送WWW-Authenticate
头部。
-
否则,如果发生
AccessDeniedException
,则*拒绝访问*。调用AccessDeniedHandler
处理拒绝访问。
如果应用程序没有抛出 |
ExceptionTranslationFilter
的伪代码看起来像这样:
try {
filterChain.doFilter(request, response); (1)
} catch (AccessDeniedException | AuthenticationException ex) {
if (!authenticated || ex instanceof AuthenticationException) {
startAuthentication(); (2)
} else {
accessDenied(); (3)
}
}
1 | 如Filter 回顾中所述,调用 FilterChain.doFilter(request, response) 等同于调用应用程序的其余部分。这意味着如果应用程序的另一部分(FilterSecurityInterceptor 或方法安全)抛出 AuthenticationException 或 AccessDeniedException ,它会在这里被捕获和处理。 |
2 | 如果用户未认证或发生 AuthenticationException ,则*开始认证*。 |
3 | 否则,*拒绝访问*。 |
认证期间保存请求
如处理安全异常所示,当请求未认证且需要认证的资源时,需要保存针对已认证资源的请求,以便在认证成功后重新发送。在 Spring Security 中,这通过使用 RequestCache
实现保存 HttpServletRequest
来完成。
RequestCache
HttpServletRequest
保存在 RequestCache
中。当用户成功认证后,RequestCache
被用来重放原始请求。RequestCacheAwareFilter
在用户认证后使用 RequestCache
获取保存的 HttpServletRequest
,而 ExceptionTranslationFilter
在检测到 AuthenticationException
后,在将用户重定向到登录端点之前,使用 RequestCache
保存 HttpServletRequest
。
默认情况下,使用 HttpSessionRequestCache
。下面的代码演示了如何自定义 RequestCache
实现,该实现仅在存在名为 continue
的参数时才检查 HttpSession
中是否有保存的请求。
continue
参数时,RequestCache
才检查保存的请求-
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>