架构
过滤器回顾
Spring Security 的 Servlet 支持基于 Servlet 过滤器,因此首先了解过滤器的作用很有帮助。下图显示了单个 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
中的Security Filters通常是 Bean,但它们是注册到FilterChainProxy
而不是DelegatingFilterProxy中的。FilterChainProxy
相较于直接向 Servlet 容器或DelegatingFilterProxy注册,提供了许多优势。首先,它为所有 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
实例。
Security Filters
安全过滤器使用SecurityFilterChain API 插入到FilterChainProxy中。这些过滤器可用于许多不同的目的,例如身份验证、授权、漏洞防护等等。这些过滤器按特定顺序执行,以保证它们在正确的时间被调用,例如,执行身份验证的Filter
应该在执行授权的Filter
之前调用。通常不需要知道 Spring Security 的Filter
的顺序。但是,有时了解顺序是有益的,如果您想了解它们,您可以查看FilterOrderRegistration
代码。
为了举例说明上一段,让我们考虑以下安全配置
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(Customizer.withDefaults())
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.formLogin(Customizer.withDefaults());
return http.build();
}
}
import org.springframework.security.config.web.servlet.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
csrf { }
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
httpBasic { }
formLogin { }
}
return http.build()
}
}
上述配置将导致以下Filter
顺序
过滤器 | 添加者 |
---|---|
|
|
|
|
|
|
|
-
首先,调用
CsrfFilter
以防止CSRF 攻击。 -
其次,调用身份验证过滤器以对请求进行身份验证。
-
第三,调用
AuthorizationFilter
以授权请求。
可能还有其他未列出的 |
打印安全过滤器
通常情况下,查看为特定请求调用的安全Filter
列表非常有用。例如,您想确保您添加的过滤器位于安全过滤器列表中。
过滤器列表在应用程序启动时以 INFO 级别打印,因此您可以在控制台输出中看到类似以下内容:
2023-06-14T08:55:22.321-03:00 INFO 76975 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [
org.springframework.security.web.session.DisableEncodeUrlFilter@404db674,
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@50f097b5,
org.springframework.security.web.context.SecurityContextHolderFilter@6fc6deb7,
org.springframework.security.web.header.HeaderWriterFilter@6f76c2cc,
org.springframework.security.web.csrf.CsrfFilter@c29fe36,
org.springframework.security.web.authentication.logout.LogoutFilter@ef60710,
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@7c2dfa2,
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@4397a639,
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@7add838c,
org.springframework.security.web.authentication.www.BasicAuthenticationFilter@5cc9d3d0,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@7da39774,
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@32b0876c,
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@3662bdff,
org.springframework.security.web.access.ExceptionTranslationFilter@77681ce4,
org.springframework.security.web.access.intercept.AuthorizationFilter@169268a7]
这将很好地了解为每个过滤器链配置的安全过滤器。
但这并非全部,您还可以配置您的应用程序以打印每个请求的每个单独过滤器的调用。这有助于查看您添加的过滤器是否为特定请求调用,或者检查异常来自何处。为此,您可以配置您的应用程序以记录安全事件。
向过滤器链添加自定义过滤器
大多数情况下,默认的安全过滤器足以向您的应用程序提供安全保障。但是,有时您可能想要向安全过滤器链添加自定义Filter
。
例如,假设您想要添加一个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 | 如果用户有访问权限,则调用链中的其余过滤器。 |
4 | 如果用户没有访问权限,则抛出AccessDeniedException 。 |
与其实现 |
现在,我们需要将过滤器添加到安全过滤器链中。
-
Java
-
Kotlin
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// ...
.addFilterBefore(new TenantFilter(), AuthorizationFilter.class); (1)
return http.build();
}
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http
// ...
.addFilterBefore(TenantFilter(), AuthorizationFilter::class.java) (1)
return http.build()
}
1 | 使用HttpSecurity#addFilterBefore 在AuthorizationFilter 之前添加TenantFilter 。 |
通过在AuthorizationFilter
之前添加过滤器,我们确保在身份验证过滤器之后调用TenantFilter
。您还可以使用HttpSecurity#addFilterAfter
在特定过滤器之后添加过滤器,或者使用HttpSecurity#addFilterAt
在过滤器链中的特定过滤器位置添加过滤器。
就是这样,现在TenantFilter
将在过滤器链中调用,并将检查当前用户是否具有对租户 ID 的访问权限。
当您将过滤器声明为 Spring bean 时(例如,通过使用@Component
对其进行注释或将其声明为 bean),请小心,因为 Spring Boot 将自动将其注册到嵌入式容器。这可能会导致过滤器被调用两次,一次由容器调用,一次由 Spring Security 调用,并且顺序不同。
如果您仍然希望将过滤器声明为 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;
}
处理安全异常
ExceptionTranslationFilter
作为安全过滤器之一插入到FilterChainProxy中。
下图显示了ExceptionTranslationFilter
与其他组件的关系

-
首先,
ExceptionTranslationFilter
调用FilterChain.doFilter(request, response)
来调用应用程序的其余部分。 -
如果用户未经身份验证或它是
AuthenticationException
,则启动身份验证。-
SecurityContextHolder
被清除。 -
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 | 如过滤器回顾中所述,调用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 https://127.0.0.1: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>