对于最新的稳定版本,请使用 Spring Security 6.5.3! |
架构
过滤器回顾
Spring Security 的 Servlet 支持基于 Servlet 过滤器,因此首先查看过滤器的作用会很有帮助。 下图显示了单个 HTTP 请求的处理程序的典型分层。

客户端向应用程序发送请求,容器创建FilterChain
,其中包含Filter
instances 和Servlet
这应该处理HttpServletRequest
,基于请求 URI 的路径。
在 Spring MVC 应用程序中,Servlet
是DispatcherServlet
.
最多一个Servlet
可以处理单个HttpServletRequest
和HttpServletResponse
.
但是,不止一个Filter
可用于:
-
防止下游
Filter
实例或Servlet
避免被调用。 在这种情况下,Filter
通常将HttpServletResponse
. -
修改
HttpServletRequest
或HttpServletResponse
下游Filter
实例和Servlet
.
的力量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
被调用非常重要。
委托过滤器代理
Spring 提供了一个Filter
名为DelegatingFilterProxy
允许在 Servlet 容器的生命周期和 Spring 的生命周期之间桥接ApplicationContext
.
Servlet 容器允许注册Filter
实例,但它不知道 Spring 定义的 Bean。
您可以注册DelegatingFilterProxy
通过标准的 Servlet 容器机制,但将所有工作委托给实现Filter
.
这是如何DelegatingFilterProxy
适合Filter
实例和FilterChain
.

DelegatingFilterProxy
抬头看豆过滤器0从ApplicationContext
然后调用豆过滤器0.
以下列表显示了DelegatingFilterProxy
:
DelegatingFilterProxy
伪代码-
Java
-
Kotlin
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
// Lazily get Filter that was registered as a Spring Bean
// For the example in DelegatingFilterProxy delegate
is an instance of Bean Filter0
Filter delegate = getFilterBean(someBeanName);
// delegate work to the Spring Bean
delegate.doFilter(request, response);
}
fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
// Lazily get Filter that was registered as a Spring Bean
// For the example in DelegatingFilterProxy delegate
is an instance of Bean Filter0
val delegate: Filter = getFilterBean(someBeanName)
// delegate work to the Spring Bean
delegate.doFilter(request, response)
}
另一个好处DelegatingFilterProxy
是它允许延迟查找Filter
bean 实例。
这很重要,因为容器需要注册Filter
实例,然后容器才能启动。
但是,Spring 通常使用ContextLoaderListener
加载 Spring Beans,这要等到Filter
实例需要注册。
过滤器链代理
Spring Security 的 Servlet 支持包含在FilterChainProxy
.FilterChainProxy
是一个特殊的Filter
由 Spring Security 提供,允许委托给许多Filter
实例通过SecurityFilterChain
.
因为FilterChainProxy
是一个 Bean,它通常包装在 DelegatingFilterProxy 中。
下图显示了FilterChainProxy
.

安全过滤器链
SecurityFilterChain
FilterChainProxy 使用哪个 Spring SecurityFilter
应为当前请求调用实例。
下图显示了SecurityFilterChain
.

中的安全过滤器SecurityFilterChain
通常是 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
可以根据HttpServletRequest
通过使用RequestMatcher
接口。
下图显示了多个SecurityFilterChain
实例:

在 Multiple SecurityFilterChain 图中,FilterChainProxy
决定哪个SecurityFilterChain
应该使用。
只有第一个SecurityFilterChain
调用匹配项。
如果 URL 为/api/messages/
,它首先在SecurityFilterChain0
模式/api/**
,所以只有SecurityFilterChain0
被调用,即使它也匹配SecurityFilterChainn
.
如果 URL 为/messages/
,则在SecurityFilterChain0
模式/api/**
所以FilterChainProxy
继续尝试每个SecurityFilterChain
.
假设没有其他SecurityFilterChain
实例匹配,SecurityFilterChainn
被调用。
请注意SecurityFilterChain0
只有三个安全性Filter
实例配置。
然而SecurityFilterChainn
有四个安全性Filter
实例配置。
需要注意的是,每个SecurityFilterChain
可以是唯一的,并且可以单独配置。
事实上,一个SecurityFilterChain
可能没有安全性Filter
如果应用程序希望 Spring Security 忽略某些请求。
安全过滤器
安全过滤器使用 SecurityFilterChain API 插入到 FilterChainProxy 中。
这些过滤器可用于许多不同的目的,例如身份验证、授权、漏洞利用保护等。
过滤器按特定顺序执行,以保证在正确的时间调用它们,例如Filter
执行身份验证的 应在Filter
执行授权。
通常没有必要知道 Spring Security 的Filter
s.
但是,有时了解顺序是有益的,如果您想了解它们,可以查看FilterOrderRegistration
法典.
为了举例说明上面的段落,让我们考虑以下安全配置:
@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
订购:
Filter | 添加者 |
---|---|
|
|
|
|
|
|
|
-
首先,
CsrfFilter
被调用以防止 CSRF 攻击。 -
其次,调用身份验证过滤器来验证请求。
-
第三,该
AuthorizationFilter
被调用以授权请求。
可能还有其他 |
打印安全过滤器
通常,查看安全列表很有用Filter
s 来调用特定请求。
例如,您要确保已添加的过滤器位于安全过滤器列表中。
筛选器列表在应用程序启动时以 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 . |
而不是实施 |
现在,我们需要将过滤器添加到安全过滤器链中。
@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 添加TenantFilter 在AuthorizationFilter . |
通过在AuthorizationFilter
我们正在确保TenantFilter
在身份验证筛选器之后调用。
您还可以使用HttpSecurity#addFilterAfter
在特定过滤器之后添加过滤器,或HttpSecurity#addFilterAt
在过滤器链中的特定过滤器位置添加过滤器。
就是这样,现在TenantFilter
将在筛选器链中调用,并将检查当前用户是否有权访问租户 ID。
将过滤器声明为 Spring Bean 时要小心,只需使用@Component
或者通过在配置中将其声明为 bean,因为 Spring Boot 会自动将其注册到嵌入式容器中。
这可能会导致过滤器被调用两次,一次由容器调用,一次由 Spring Security 调用,并且以不同的顺序调用。
例如,如果您仍想将过滤器声明为 Spring bean 以利用依赖注入并避免重复调用,您可以通过声明FilterRegistrationBean
bean 并设置其enabled
属性设置为false
:
@Bean
public FilterRegistrationBean<TenantFilter> tenantFilterRegistration(TenantFilter filter) {
FilterRegistrationBean<TenantFilter> registration = new FilterRegistrationBean<>(filter);
registration.setEnabled(false);
return registration;
}
处理安全异常
这ExceptionTranslationFilter
允许翻译AccessDeniedException
和AuthenticationException
转换为 HTTP 响应。
ExceptionTranslationFilter
作为安全过滤器之一插入到 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 | 如过滤器审查中所述,调用FilterChain.doFilter(request, response) 等效于调用应用程序的其余部分。这意味着,如果应用程序的另一部分 (FilterSecurityInterceptor 或方法安全性)会抛出AuthenticationException 或AccessDeniedException 它在这里被捕获和处理。 |
2 | 如果用户未经过身份验证或是AuthenticationException ,开始身份验证。 |
3 | 否则,访问被拒绝 |
在身份验证之间保存请求
如处理安全异常中所示,当请求没有身份验证并且是针对需要身份验证的资源时,需要保存请求,以便经过身份验证的资源在身份验证成功后重新请求。在 Spring Security 中,这是通过保存HttpServletRequest
使用RequestCache
实现。
请求缓存
这HttpServletRequest
保存在RequestCache
. 当用户成功进行身份验证时,RequestCache
用于重放原始请求。 这RequestCacheAwareFilter
是使用RequestCache
以保存HttpServletRequest
.
默认情况下,一个HttpSessionRequestCache
被使用。下面的代码演示了如何自定义RequestCache
用于检查HttpSession
对于已保存的请求,如果参数名为continue
存在。
RequestCache
仅在以下情况下检查已保存的请求continue
参数存在-
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
使用RequestCache
以保存HttpServletRequest
.
Logging
Spring Security 在 DEBUG 和 TRACE 级别提供所有安全相关事件的全面日志记录。 这在调试应用程序时非常有用,因为对于安全措施,Spring Security 不会在响应正文中添加请求被拒绝原因的任何详细信息。 如果您遇到 401 或 403 错误,您很可能会找到一条日志消息,帮助您了解发生了什么。
让我们考虑一个示例,其中用户尝试将POST
请求到启用了 CSRF 保护而不使用 CSRF Tokens的资源。
如果没有日志,用户将看到 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 Tokens丢失,这就是请求被拒绝的原因。
要将应用程序配置为记录所有安全事件,您可以向应用程序添加以下内容:
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>