过滤器综述
Spring Security 的 Servlet 支持基于 Servlet 过滤器,因此首先查看过滤器的作用是有帮助的。 下图显示了单个 HTTP 请求的处理程序的典型分层。
![过滤链](https://docs.spring.io/spring-security/reference/6.3/_images/servlet/architecture/filterchain.png)
客户端向应用程序发送请求,容器创建一个 ,其中包含实例,并且应根据请求 URI 的路径处理 。
在 Spring MVC 应用程序中,是 DispatcherServlet
的实例。
最多可以处理单个 和 .
但是,可以使用多个方法:FilterChain
Filter
Servlet
HttpServletRequest
Servlet
Servlet
HttpServletRequest
HttpServletResponse
Filter
-
防止下游实例或 被调用。 在本例中,通常会写入 .
Filter
Servlet
Filter
HttpServletResponse
-
修改下游实例使用的 or 和 .
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
}
由于 a 只影响下游实例,而 ,因此每个实例的调用顺序非常重要。Filter
Filter
Servlet
Filter
DelegatingFilterProxy
Spring 提供了一个名为 DelegatingFilterProxy
的实现,它允许在 Servlet 容器的生命周期和 Spring 的 .
Servlet 容器允许使用自己的标准注册实例,但它不知道 Spring 定义的 Beans。
您可以通过标准的 Servlet 容器机制进行注册,但将所有工作委托给实现 .Filter
ApplicationContext
Filter
DelegatingFilterProxy
Filter
下面是 Filter
实例和 FilterChain
的适应情况。DelegatingFilterProxy
![委派FilterProxy](https://docs.spring.io/spring-security/reference/6.3/_images/servlet/architecture/delegatingfilterproxy.png)
DelegatingFilterProxy
抬头查找豆过滤器0从 然后调用ApplicationContext
豆过滤器0.
以下列表显示了 的伪代码: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 的过滤器。
例如,在 DelegatingFilterProxy 中,是delegate 豆过滤器0. |
2 | 将工作委托给 Spring Bean。 |
另一个好处是它允许延迟查找 Bean 实例。
这很重要,因为容器需要先注册实例,然后才能启动容器。
但是,Spring 通常使用 a 来加载 Spring Beans,直到需要注册实例后才会完成。DelegatingFilterProxy
Filter
Filter
ContextLoaderListener
Filter
1 | 懒惰地获取已注册为 Spring Bean 的过滤器。
例如,在 DelegatingFilterProxy 中,是delegate 豆过滤器0. |
2 | 将工作委托给 Spring Bean。 |
过滤器链代理
Spring Security 的 Servlet 支持包含在 中。 是 Spring Security 提供的一项特殊功能,允许通过 SecurityFilterChain
委托给多个实例。
由于是 Bean,因此它通常包装在 DelegatingFilterProxy 中。FilterChainProxy
FilterChainProxy
Filter
Filter
FilterChainProxy
下图显示了 的作用。FilterChainProxy
![filterchain代理](https://docs.spring.io/spring-security/reference/6.3/_images/servlet/architecture/filterchainproxy.png)
安全过滤器链
FilterChainProxy 使用 SecurityFilterChain
来确定应为当前请求调用哪些 Spring Security 实例。Filter
下图显示了 的作用。SecurityFilterChain
![安全过滤器链](https://docs.spring.io/spring-security/reference/6.3/_images/servlet/architecture/securityfilterchain.png)
中的安全过滤器通常是 Beans,但它们是注册的,而不是 DelegatingFilterProxy。 为直接向 Servlet 容器或 DelegatingFilterProxy 注册提供了许多优点。
首先,它为Spring Security的所有Servlet支持提供了一个起点。
出于这个原因,如果您尝试对 Spring Security 的 Servlet 支持进行故障排除,那么添加调试点是一个很好的起点。SecurityFilterChain
FilterChainProxy
FilterChainProxy
FilterChainProxy
其次,由于它是 Spring Security 使用的核心,因此它可以执行不被视为可选的任务。
例如,它会清除以避免内存泄漏。
它还应用 Spring Security 的 HttpFirewall
来保护应用程序免受某些类型的攻击。FilterChainProxy
SecurityContext
此外,它还在确定何时应调用 a 方面提供了更大的灵活性。
在 Servlet 容器中,仅根据 URL 调用实例。
但是,可以使用接口根据其中的任何内容确定调用。SecurityFilterChain
Filter
FilterChainProxy
HttpServletRequest
RequestMatcher
下图显示了多个实例:SecurityFilterChain
![多重安全过滤器链](https://docs.spring.io/spring-security/reference/6.3/_images/servlet/architecture/multi-securityfilterchain.png)
在 Multiple SecurityFilterChain 图中,确定应使用哪个。
仅调用第一个匹配的。
如果请求 的 URL,则它首先与 的模式匹配,因此仅调用,即使它也匹配 。
如果请求的 URL 与 的模式不匹配,则继续尝试每个 .
假设没有其他实例匹配,则调用。FilterChainProxy
SecurityFilterChain
SecurityFilterChain
/api/messages/
SecurityFilterChain0
/api/**
SecurityFilterChain0
SecurityFilterChainn
/messages/
SecurityFilterChain0
/api/**
FilterChainProxy
SecurityFilterChain
SecurityFilterChain
SecurityFilterChainn
请注意,仅配置了三个安全实例。
但是,配置了四个安全实例。
需要注意的是,每个都可以是唯一的,并且可以单独配置。
事实上,如果应用程序希望 Spring Security 忽略某些请求,则 a 的安全实例可能为零。SecurityFilterChain0
Filter
SecurityFilterChainn
Filter
SecurityFilterChain
SecurityFilterChain
Filter
安全过滤器
使用 SecurityFilterChain API 将安全过滤器插入到 FilterChainProxy 中。
这些过滤器可用于许多不同的目的,例如身份验证、授权、漏洞利用保护等。
筛选器按特定顺序执行,以保证在正确的时间调用它们,例如,应先调用执行身份验证的筛选器,然后再调用执行授权的筛选器。
通常没有必要知道 Spring Security 的顺序。
但是,有时了解排序是有益的,如果您想了解它们,可以查看 FilterOrderRegistration
代码。Filter
Filter
Filter
为了举例说明上面的段落,让我们考虑以下安全配置:
-
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
滤波器 | 添加者 |
---|---|
|
|
|
|
|
|
|
-
首先,调用 来防止 CSRF 攻击。
CsrfFilter
-
其次,调用身份验证筛选器来对请求进行身份验证。
-
第三,调用 来授权请求。
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
例如,假设要添加一个获取租户 ID 标头的 it,并检查当前用户是否有权访问该租户。
前面的描述已经为我们提供了在何处添加过滤器的线索,因为我们需要知道当前用户,我们需要在身份验证过滤器之后添加它。Filter
首先,让我们创建: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 |
您可以从 OncePerRequestFilter 扩展,而不是实现 ,OncePerRequestFilter 是过滤器的基类,每个请求仅调用一次,并提供具有 and 参数的方法。 |
现在,我们需要将过滤器添加到安全过滤器链中。
-
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 TenantFilter AuthorizationFilter |
通过在身份验证过滤器之前添加过滤器,我们确保在身份验证过滤器之后调用。
还可以用于在特定过滤器之后添加过滤器,或在过滤器链中的特定过滤器位置添加过滤器。AuthorizationFilter
TenantFilter
HttpSecurity#addFilterAfter
HttpSecurity#addFilterAt
就是这样,现在将在筛选器链中调用,并检查当前用户是否有权访问租户 ID。TenantFilter
当您将过滤器声明为 Spring Bean 时,无论是使用它进行注释还是在配置中将其声明为 Bean,都要小心,因为 Spring Boot 会自动将其注册到嵌入式容器中。
这可能会导致过滤器被调用两次,一次由容器调用,一次由Spring Security调用,顺序不同。@Component
例如,如果您仍然想将过滤器声明为 Spring Bean 以利用依赖注入,并避免重复调用,您可以通过声明 Bean 并将其属性设置为 :FilterRegistrationBean
enabled
false
@Bean
public FilterRegistrationBean<TenantFilter> tenantFilterRegistration(TenantFilter filter) {
FilterRegistrationBean<TenantFilter> registration = new FilterRegistrationBean<>(filter);
registration.setEnabled(false);
return registration;
}
滤波器 | 添加者 |
---|---|
|
|
|
|
|
|
|
可能还有其他未在上面列出的实例。
如果要查看为特定请求调用的筛选器列表,可以打印它们。 |
1 | 从请求标头中获取租户 ID。 |
2 | 检查当前用户是否有权访问租户 ID。 |
3 | 如果用户具有访问权限,则调用链中的其余筛选器。 |
4 | 如果用户无权访问,则抛出 .AccessDeniedException |
您可以从 OncePerRequestFilter 扩展,而不是实现 ,OncePerRequestFilter 是过滤器的基类,每个请求仅调用一次,并提供具有 and 参数的方法。 |
1 | 用于在 .HttpSecurity#addFilterBefore TenantFilter AuthorizationFilter |
处理安全异常
ExceptionTranslationFilter
允许将 AccessDeniedException
和 AuthenticationException
转换为 HTTP 响应。
ExceptionTranslationFilter
作为安全过滤器之一插入到 FilterChainProxy 中。
下图显示了与其他组件的关系:ExceptionTranslationFilter
![ExceptionTranslationFilter](https://docs.spring.io/spring-security/reference/6.3/_images/servlet/architecture/exceptiontranslationfilter.png)
-
首先,调用以调用应用程序的其余部分。
ExceptionTranslationFilter
FilterChain.doFilter(request, response)
-
如果用户未通过身份验证或该用户是 ,则启动身份验证。
AuthenticationException
-
保存 ,以便在身份验证成功后可以使用它来重播原始请求。
HttpServletRequest
-
用于从客户端请求凭据。 例如,它可能会重定向到登录页面或发送标头。
AuthenticationEntryPoint
WWW-Authenticate
-
否则,如果它是 ,则拒绝访问。 调用 来处理拒绝的访问。
AccessDeniedException
AccessDeniedHandler
如果应用程序没有抛出 an 或 ,则不执行任何操作。 |
的伪代码如下所示:ExceptionTranslationFilter
try {
filterChain.doFilter(request, response); (1)
} catch (AccessDeniedException | AuthenticationException ex) {
if (!authenticated || ex instanceof AuthenticationException) {
startAuthentication(); (2)
} else {
accessDenied(); (3)
}
}
1 | 如筛选器评审中所述,调用等同于调用应用程序的其余部分。
这意味着,如果应用程序的另一部分(FilterSecurityInterceptor 或方法安全性)抛出 或在此处捕获和处理。FilterChain.doFilter(request, response) AuthenticationException AccessDeniedException |
2 | 如果用户未通过身份验证或用户是 ,则启动身份验证。AuthenticationException |
3 | 否则,访问被拒绝 |
如果应用程序没有抛出 an 或 ,则不执行任何操作。 |
1 | 如筛选器评审中所述,调用等同于调用应用程序的其余部分。
这意味着,如果应用程序的另一部分(FilterSecurityInterceptor 或方法安全性)抛出 或在此处捕获和处理。FilterChain.doFilter(request, response) AuthenticationException AccessDeniedException |
2 | 如果用户未通过身份验证或用户是 ,则启动身份验证。AuthenticationException |
3 | 否则,访问被拒绝 |
在身份验证之间保存请求
如处理安全异常中所述,当请求没有身份验证并且针对需要身份验证的资源时,需要保存请求,以便身份验证成功后重新请求经过身份验证的资源。
在 Spring Security 中,这是通过使用 RequestCache
实现保存来完成的。HttpServletRequest
请求缓存
保存在 RequestCache
中。
当用户成功进行身份验证时,将用于重播原始请求。
RequestCacheAwareFilter
使用 to 获取用户身份验证后保存的内容,而 使用 to 在检测到 之后保存 ,然后再将用户重定向到登录终结点。HttpServletRequest
RequestCache
RequestCache
HttpServletRequest
ExceptionTranslationFilter
RequestCache
HttpServletRequest
AuthenticationException
默认情况下,使用 an。
下面的代码演示了如何自定义用于检查已保存请求的实现(如果存在 named 参数)。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
RequestCacheAwareFilter
使用 RequestCache
重播原始请求。
Logging
Spring Security 在 DEBUG 和 TRACE 级别提供所有安全相关事件的全面日志记录。 这在调试应用程序时非常有用,因为出于安全措施,Spring Security 不会向响应正文添加任何有关请求被拒绝原因的详细信息。 如果您遇到 401 或 403 错误,您很可能会找到一条日志消息,帮助您了解正在发生的事情。
让我们考虑一个示例,其中用户尝试向启用了 CSRF 保护的资源发出请求,而无需 CSRF 令牌。
如果没有日志,用户将看到 403 错误,其中没有解释请求被拒绝的原因。
但是,如果为 Spring Security 启用日志记录,您将看到如下日志消息:POST
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>