架构
本节讨论了 Spring Security 在基于 Servlet 的应用程序中的高层架构。我们将在参考中的“认证”、“授权”和“防止攻击”部分进一步基于这一高层理解进行讨论。
过滤器概览
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`。
过滤器的力量来自于传入的 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
}
由于一个只影响下游的实例和 ,因此每个被调用的顺序极其重要。
DelegatingFilterProxy
Spring 提供了一个名为 DelegatingFilterProxy 的 实现,它允许在 Servlet 容器生命周期和 Spring 的 >之间进行桥梁作用。Servlet 容器允许通过使用<标准##stand>规范来注册实例,但并不了解 Spring 定义的 Bean。你可以通过标准 Servlet 容器机制注册 ,但将实际的过滤逻辑委托给一个实现了的 Spring Bean 来。
这里是一个在实例和中的位置的图片。
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,而 Bean 的加载是在需要注册`Filter`实例之后进行的。
FilterChainProxy
Spring Security 的 Servlet 支持包含在`FilterChainProxy`中。
`FilterChainProxy`是 Spring Security 提供的一个特殊`Filter`,它允许通过`SecurityFilterChain`委托给多个`Filter`实例。由于`FilterChainProxy`是一个 Bean,它通常会被包装在`DelegatingFilterProxy`中。
以下图片展示了`FilterChainProxy`的作用。
图 3. FilterChainProxy
SecurityFilterChain
SecurityFilterChain 由 FilterChainProxy 使用来确定当前请求应该调用哪些 Spring Security Filter 实例。
以下图片展示了`SecurityFilterChain`的作用。
图 4. SecurityFilterChain
在 `` 中的的 `` 通常为 Beans, 但它们是注册到 `` 而不是 ``。
`FilterChainProxy` 相比直接注册到 Servlet 容器或 `DelegatingFilterProxy` 提供了多项优势。首先,它为 Spring Security 的所有 Servlet 支持提供了一个起点。因此,如果你要调试 Spring Security 的 Servlet 支持,将调试点添加到 `FilterChainProxy` 是一个很好的起点。
第二
由于 `FilterChainProxy` 在 Spring Security 中居于核心地位,因此可以执行一些可是可可选的任务。例如,,会清空 `SecurityContext` 以避免内存泄漏。此外, `FilterChainProxy` 会根据 `` 中的 `` 接口来来决定何时调用 ``。
此外,为了在决定何时调用 `` 时时提供更多的灵活性。在 Servlet 容器中, `` 实例是根据 URL 单独调用的。然而但是 `` 会根据 `` 中的的 `` 接口来来决定何时调用。
以下图片展示了多个`SecurityFilterChain`实例:
图 5. 多个 SecurityFilterChain
在图中,`FilterChainProxy`决定使用哪个`SecurityFilterChain`。只有第一个匹配的`SecurityFilterChain`会被调用。如果请求的是`/api/messages/`,它首先匹配`SecurityFilterChain_0`的模式`/api/**`,因此只有`SecurityFilterChain_0`会被调用,即使它也匹配`SecurityFilterChain_n`。如果请求的是`/messages/`,它不会匹配`SecurityFilterChain_0`的模式`/api/**`,因此`FilterChainProxy`会继续尝试每个`SecurityFilterChain`。假设没有其他`SecurityFilterChain`实例匹配,`SecurityFilterChain_n`会被调用。
请注意,SecurityFilterChain0 仅配置了三个安全过滤器实例。然而,SecurityFilterChainn 配置了四个安全过滤器实例。需要注意的是,每个 SecurityFilterChain 可以是唯一的,并且可以独立配置。实际上,如果应用程序希望 Spring Security 忽略某些请求,SecurityFilterChain 可以没有任何安全过滤器实例。
安全过滤器
这些过滤器通过 SecurityFilterChain API 插入到 FilterChainProxy 中。
这些过滤器可以用于多种不同的目的,比如
利用防护 、 认证 、 授权 ,以及其他功能。这些过滤器会按照特定的顺序执行,以确保它们在正确的时间被调用,例如,执行认证的过滤器应该在执行授权的过滤器之前被调用。通常情况下,不需要知道 Spring Security 过滤器的顺序。然而,有时了解这些顺序是有益的,如果你需要知道它们,可以查看 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()
}
}
上述配置将导致以下过滤器顺序:
| 过滤器 | 添加了 |
|---|---|
|
|
|
|
|
|
|
-
首先,`CsrfFilter` 被调用以防止跨站请求伪造攻击。 -
其次,认证过滤器被调用以认证请求。 -
最后,`AuthorizationFilter` 被调用以授权请求。
|
|
打印安全 <code>Filter</code> 列表
很多时候,查看特定请求调用了哪些安全 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]
这将大致了解为每个过滤器链配置的安全过滤器。
但这还远不止这些,你还可以配置应用程序,使其为每个请求打印每个单独过滤器的调用。这有助于查看你添加的过滤器是否被特定请求调用,或者检查异常来自何处。为此,你可以配置应用程序 记录安全事件 。
向过滤器链添加过滤器
大多数情况下,默认的 安全过滤器 已经足够为你的应用程序提供安全保护。然而,有时你可能希望向 SecurityFilterChain 添加一个自定义的 `Filter`。
HttpSecurity 提供了三种方法来添加过滤器:
-
#addFilterBefore(Filter, Class<?>)在另一个过滤器之前添加您的过滤器 -
#addFilterAfter(Filter, Class<?>)在另一个过滤器之后添加您的过滤器 -
#addFilterAt(Filter, Class<?>)用您的过滤器替换另一个过滤器
添加自定义过滤器
如果你正在创建自己的过滤器,你需要确定它在过滤器链中的位置。请参阅过滤器链中发生的以下关键事件:
-
`SecurityContext` 从会话中加载 -
请求受到常见攻击的保护;`secure headers`、`CORS`、`CSRF` -
请求已认证 -
请求已授权
考虑为了定位你的过滤器,需要发生哪些事件。以下是一个经验法则:
如果您的过滤器是( |
则将其放置在( |
因为这些事件已经发生 |
|---|---|---|
|
|
1 |
认证过滤器 |
注销过滤器 |
1, 2 |
授权过滤器 |
|
1, 2, 3 |
通常情况下,应用程序会添加自定义认证。这意味着它们应该放在 LogoutFilter 之后。 |
例如,假设你想添加一个过滤器,该过滤器获取租户 ID 头,并检查当前用户是否有访问该租户的权限。
首先,让我们创建该过滤器:
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 方法,在 AnonymousAuthenticationFilter 之后添加 TenantFilter。 |
通过在 AnonymousAuthenticationFilter 过滤器之后添加过滤器,我们确保了 TenantFilter 在认证过滤器之后被调用。
现在就完成了,TenantFilter 将会被过滤器链调用,并检查当前用户是否有访问租户 ID 的权限。
声明你的过滤器为一个 Bean
当你通过注解 @Component 或在配置中声明一个 Filter 时,Spring Boot 会自动将其注册到嵌入式容器中。这可能会导致过滤器被容器和 Spring Security 分别调用两次,并且调用顺序不同。
因此,过滤器通常不是 Spring bean。
然而,如果你的过滤器需要成为一个 Spring bean(例如,以便利用依赖注入),你可以告诉 Spring Boot 不要将其注册到容器中,通过声明一个`FilterRegistrationBean` bean 并将它的`enabled`属性设置为`false`:
@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 过滤器,你可以在 DSL 中使用 addFilterAt>来指定:
-
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。
|
|
处理安全异常
`ExceptionTranslationFilter` 允许将 `AccessDeniedException` 和 `AuthenticationException` 转换为 HTTP 响应。
`ExceptionTranslationFilter` 作为其中一个 `Security Filter` 被插入到 `FilterChainProxy` 中。
以下图像展示了`ExceptionTranslationFilter`与其他组件之间的关系:
-
首先,ExceptionTranslationFilter调用FilterChain.doFilter(request, response)以执行应用程序的其余部分。 -
如果用户未经过身份验证或发生 AuthenticationException,则进行身份验证。-
SecurityContextHolder被清空。 -
HttpServletRequest被保存,以便在身份验证成功后可以重新播放原始请求。 -
使用AuthenticationEntryPoint从客户端请求凭据。例如,它可能会重定向到登录页面或发送WWW-Authenticate头。
-
-
否则,如果抛出的是 AccessDeniedException,则显示“访问被拒绝”。此时,AccessDeniedHandler会被调用来处理访问被拒绝的情况。
|
|
ExceptionTranslationFilter 的伪代码类似于以下内容:
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。以下代码演示了如何自定义用于检查 HttpSession 中保存的请求的 RequestCache 实现,前提是存在名为 continue 的参数。
RequestCache 只在存在 continue 参数时检查 HttpSession 中的保存请求
-
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"/>
日志记录
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 令牌,因此请求被拒绝了。
要配置应用程序以记录所有安全事件,您可以在应用程序中添加以下内容:
application.properties 在 Spring Boot 中
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>