|
此版本仍在开发中,尚未被视为稳定版本。如需最新稳定版本,请使用 Spring Security 7.0.4! |
架构
过滤器综述
Spring Security 的 Servlet 支持基于 Servlet 过滤器(Filters),因此首先了解过滤器的一般作用会很有帮助。 下图展示了处理单个 HTTP 请求时典型的处理器分层结构。
客户端向应用程序发送请求,容器会创建一个 FilterChain,其中包含根据请求 URI 路径应处理 HttpServletRequest 的 Filter 实例和 Servlet。
在 Spring MVC 应用程序中,Servlet 是 DispatcherServlet 的一个实例。
最多只能有一个 Servlet 来处理单个 HttpServletRequest 和 HttpServletResponse。
然而,可以使用多个 Filter 来:
-
阻止下游的
Filter实例或Servlet被调用。 在这种情况下,Filter通常会写入HttpServletResponse。 -
修改下游
HttpServletRequest实例和HttpServletResponse所使用的Filter或Servlet。
Filter 的强大之处在于传入其中的 FilterChain。
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被调用的顺序至关重要。
委托过滤器代理
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 实例之后才完成的。
过滤器链代理
Spring Security 的 Servlet 支持包含在 FilterChainProxy 中。
FilterChainProxy 是 Spring Security 提供的一个特殊 Filter,它允许通过 SecurityFilterChain 委托给多个 Filter 实例。
由于 FilterChainProxy 是一个 Bean,它通常被包装在 DelegatingFilterProxy 中。
下图展示了 FilterChainProxy 的作用。
安全过滤器链
SecurityFilterChain 被 FilterChainProxy 用于确定当前请求应调用哪些 Spring Security Filter 实例。
下图展示了 SecurityFilterChain 的作用。
#servlet-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 可以通过使用 HttpServletRequest 接口,根据 RequestMatcher 中的任意内容来决定是否调用。
下图展示了多个 SecurityFilterChain 实例:
在 多个 SecurityFilterChain 图中,FilterChainProxy 决定应使用哪个 SecurityFilterChain。
仅调用第一个匹配的 SecurityFilterChain。
如果请求的 URL 为 /api/messages/,它首先匹配 /api/** 的 SecurityFilterChain0 模式,因此仅调用 SecurityFilterChain0,即使它也匹配 SecurityFilterChainn。
如果请求的 URL 为 /messages/,它不匹配 /api/** 的 SecurityFilterChain0 模式,因此 FilterChainProxy 继续尝试每个 SecurityFilterChain。
假设没有其他 SecurityFilterChain 实例匹配,则调用 SecurityFilterChainn。
请注意,SecurityFilterChain0 仅配置了三个安全 Filter 实例。
然而,SecurityFilterChainn 则配置了四个安全 Filter 实例。
需要注意的是,每个 SecurityFilterChain 都可以是唯一的,并且可以独立进行配置。
事实上,如果应用程序希望 Spring Security 忽略某些请求,那么一个 SecurityFilterChain 甚至可能不包含任何安全 Filter 实例。
安全过滤器
安全过滤器通过 FilterChainProxy 和 SecurityFilterChain API 被插入到过滤器链中。
这些过滤器可用于多种不同目的,例如
漏洞防护、身份验证、授权等。
过滤器的执行顺序是特定的,以确保它们在正确的时间被调用。例如,执行身份验证的 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以防范CSRF攻击。 -
其次,身份验证过滤器被调用以对请求进行身份验证。
-
第三,调用
AuthorizationFilter来授权请求。
|
可能还存在上述未列出的其他 |
打印安全过滤器
通常,查看针对特定请求所调用的安全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]
这将很好地展示为每个过滤器链所配置的安全过滤器。
但这还不是全部,你还可以配置你的应用程序,以打印每个请求所调用的各个过滤器。 这有助于确认你添加的过滤器是否针对某个特定请求被调用,或者用于排查异常的来源。 为此,你可以配置你的应用程序记录安全事件。
将过滤器添加到过滤器链
大多数情况下,默认的安全过滤器(Security Filters)已足以为您的应用程序提供安全保障。
然而,有时您可能希望向SecurityFilterChain中添加自定义的#servlet-securityfilterchain。
HttpSecurity 提供三种添加过滤器方法:
-
#addFilterBefore(Filter, Class<?>)在另一个过滤器之前添加您的过滤器 -
#addFilterAfter(Filter, Class<?>)在另一个过滤器之后添加您的过滤器 -
#addFilterAt(Filter, Class<?>)使用您的过滤器替换另一个过滤器
添加自定义过滤器
如果你正在创建自己的过滤器,就需要确定它在过滤器链中的位置。 请查看以下在过滤器链中发生的关键事件:
考虑一下,为了定位你的过滤器,需要发生哪些事件。 以下是一条经验法则:
| 如果你的过滤器是一个 | 然后将其放置在之后 | 由于这些事件已经发生 |
|---|---|---|
漏洞利用防护过滤器 |
SecurityContextHolderFilter |
1 |
身份验证过滤器 |
注销过滤器 |
1, 2 |
授权过滤器 |
匿名认证过滤器 |
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 | 如果用户拥有访问权限,则调用链中其余的过滤器。 |
| 4 | 如果用户没有访问权限,则抛出 AccessDeniedException。 |
|
你可以不实现 |
现在,你需要将该过滤器添加到 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()
}
| 1 | 使用 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 过滤器,可以使用 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()
}
请注意,如果该过滤器已被添加,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。
|
如果你无法重新配置
|
处理安全异常
The ExceptionTranslationFilter 支持将 AccessDeniedException 和 AuthenticationException 转换为 HTTP 响应。
ExceptionTranslationFilter 作为安全过滤器之一被插入到FilterChainProxy中。
下图展示了 ExceptionTranslationFilter 与其他组件之间的关系:
-
首先,ExceptionTranslationFilter调用FilterChain.doFilter(request, response)调用应用程序的其余部分。 -
如果用户未通过身份验证或这是一个AuthenticationException,然后开始身份验证.-
SecurityContextHolder 已被清空。
-
HttpServletRequest已被保存,以便在身份验证成功后可用于重放原始请求。 -
AuthenticationEntryPoint用于向客户端请求凭据。 例如,它可能会重定向到登录页面,或发送一个WWW-Authenticate头。
-
-
否则,如果它是AccessDeniedException,然后访问被拒绝。 TheAccessDeniedHandler被调用来处理访问被拒绝的情况。
|
如果应用程序未抛出 |
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) 等同于调用应用程序的其余部分。
这意味着,如果应用程序的其他部分(AuthorizationFilter 或方法安全)抛出 AuthenticationException 或 AccessDeniedException,则会在本处被捕获并处理。 |
| 2 | 如果用户未通过身份验证,或者发生了 AuthenticationException,则开始身份验证。 |
| 3 | 否则,访问被拒绝 |
在认证之间保存请求
如处理安全异常所示,当请求未进行身份验证且针对需要身份验证的资源时,需要保存该请求,以便在身份验证成功后重新请求受保护的资源。
在 Spring Security 中,这是通过使用 RequestCache 实现来保存 HttpServletRequest 完成的。
请求缓存
HttpServletRequest 被保存在 RequestCache 中。
当用户成功通过身份验证时,RequestCache 用于重放原始请求。
RequestCacheAwareFilter 在用户通过身份验证后使用 RequestCache 获取保存的 HttpServletRequest,而 ExceptionTranslationFilter 在检测到 AuthenticationException 后使用 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"/>
防止请求被保存
你可能有多种原因不想将用户的未认证请求存储在会话(session)中。 你可能希望将该存储卸载到用户的浏览器中,或者将其存储在数据库中。 又或者,你可能希望完全关闭此功能,因为你总是希望将用户重定向到首页,而不是他们登录前尝试访问的页面。
为此,您可以使用 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 来重放原始请求。
日志记录
Spring Security 在 DEBUG 和 TRACE 级别对所有与安全相关的事件提供全面的日志记录。 这在调试应用程序时非常有用,因为出于安全考虑,Spring Security 不会在响应体中包含请求被拒绝的具体原因。 如果你遇到 401 或 403 错误,很可能会在日志中找到有助于理解问题所在的消息。
让我们考虑一个示例:用户尝试向启用了CSRF保护的资源发送exploits/csrf.html请求,但未提供CSRFTokens。
如果没有日志记录,用户将看到一个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>