Architecture
本节讨论基于 Servlet 应用程序中 Spring Security 的高级架构。我们基于 “@13” 的高级理解,在参考的 “@14” 和 “@15” 节中构建。
A Review of Filters
Spring Security 的 Servlet 支持基于 Servlet 过滤器,所以首先了解过滤器的一般作用很有帮助。下图显示单个 HTTP 请求处理程序的典型分层结构。
客户端向应用程序发送请求,容器创建 FilterChain
,其中包含根据请求 URI 的路径处理 HttpServletRequest
的 Filter
实例和 Servlet
。在 Spring MVC 应用程序中, Servlet
是 {spring-framework-reference-url}web.html#mvc-servlet[DispatcherServlet
] 的实例。最多一个 Servlet
可以处理一个 HttpServletRequest
和 HttpServletResponse
。然而,可以使用多个 Filter
来:
-
防止调用下游
Filter
实例或Servlet
。在这种情况下,Filter
通常会编写HttpServletResponse
。 -
修改下游
Filter
实例及Servlet
使用的HttpServletRequest
或HttpServletResponse
。
Filter
的强大功能来自传递给它的 FilterChain
。
FilterChain
Usage Example-
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 提供名为 {spring-framework-api-url}org/springframework/web/filter/DelegatingFilterProxy.html[DelegatingFilterProxy
] 的 Filter
实现,该实现允许在 Servlet 容器的生命周期和 Spring 的 ApplicationContext
之间建立桥梁。Servlet 容器允许使用自己的标准注册 Filter
实例,但它并不知道 Spring 定义的 Bean。您可以通过标准 Servlet 容器机制注册 DelegatingFilterProxy
,但将所有工作委托给实现 Filter
的 Spring Bean。
以下是 DelegatingFilterProxy
如何适应 <<`Filter` 实例和 FilterChain
,servlet-filters-review>> 的图片。
DelegatingFilterProxy
从 ApplicationContext
查找 Bean Filter0,然后调用 Bean Filter0。以下列表显示的 DelegatingFilterProxy
伪代码:
DelegatingFilterProxy
Pseudo Code-
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`,servlet-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
中的调试点将是一个绝佳的起点。
其次,由于 “@16” 是 Spring Security 使用核心的,因此它可以执行不被视为可选的任务。例如,它清除 “@17” 以避免内存泄漏。它还应用 Spring Security 的 “@19” 来保护应用程序免受某些类型的攻击。
此外,它为在何时调用 SecurityFilterChain
时确定更好的灵活性。在 Servlet 容器中,Filter
的实例根据 URL 单独进行调用。但是,FilterChainProxy
可以使用 RequestMatcher
接口根据 HttpServletRequest
中的任何内容来确定调用。
下图所示多 SecurityFilterChain
实例:
在 Multiple SecurityFilterChain 图中,FilterChainProxy
决定要使用哪一个 SecurityFilterChain
。调用第一个匹配到的 SecurityFilterChain
。如果请求 URL 为 /api/messages/
,它会首先匹配 /api/
, so only SecurityFilterChain0
is invoked, even though it also matches on SecurityFilterChainn
.
If a URL of /messages/
is requested, it does not match on the SecurityFilterChain0
pattern of /api/
的 SecurityFilterChain0
模式,因此 FilterChainProxy
继续尝试每个 SecurityFilterChain
。假设没有其他 SecurityFilterChain
实例匹配,则调用 SecurityFilterChainn
。
请注意,SecurityFilterChain0
仅配置了三个安全 Filter
实例。但是,SecurityFilterChainn
已配置了四个安全 Filter
实例。请务必注意,每个 SecurityFilterChain
都可以是唯一的并且可以独立配置。事实上,如果应用程序希望 Spring Security 忽略某些请求,SecurityFilterChain
可能会具有零安全 Filter
实例。
Security Filters
安全过滤器通过 SecurityFilterChain API 插入到 FilterChainProxy 中。这些过滤器可用于多种不同的目的,如 authentication 、 authorization 、 exploit protection 等。过滤器按特定顺序执行,以保证它们在正确的时间被调用,例如,执行身份验证的 Filter
应在执行授权的 Filter
之前被调用。通常没有必要了解 Spring 安全的 Filter`s.
However, there are times that it is beneficial to know the ordering, if you want to know them, you can check the https://github.com/spring-projects/spring-security/tree/{gh-tag}/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java[`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
顺序:
Filter | Added by |
---|---|
|
|
|
|
|
|
|
-
首先,调用
CsrfFilter
以防止 CSRF attacks。 -
其次,调用身份验证筛选器以验证请求。
-
第三,调用
AuthorizationFilter
以授权请求。
可能还有其他未列出的 |
Printing the Security Filters
通常情况下,查看针对特定请求调用的安全 Filter
列表会很有用。例如,您希望确保 filter you have added 在安全过滤器列表中。
该过滤器列表在应用程序启动时以 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]
这将很好地展示针对 each filter chain 配置的安全过滤器。
但并非如此,您还可配置应用程序打印针对各个请求单独调用各个过滤器的过程。这有助于查看您添加的过滤器是否针对特定请求调用,或查看异常从何处产生。为此,您可以配置应用程序 log the security events。
Adding a Custom Filter to the Filter Chain
大多数情况下,默认安全过滤器足以用于向应用程序提供安全性。但是,有时您可能希望向安全过滤器链添加一个自定义 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 。 |
您可以扩展 {spring-framework-api-url}org/springframework/web/filter/OncePerRequestFilter.html[OncePerRequestFilter],而不实现 |
现在,我们需要将过滤器添加到安全过滤器链中。
-
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 安全,而且调用顺序不同。
如果您仍然希望将过滤器声明为 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;
}
Handling Security Exceptions
{security-api-url}org/springframework/security/web/access/ExceptionTranslationFilter.html[ExceptionTranslationFilter
] 允许将 {security-api-url}org/springframework/security/access/AccessDeniedException.html[AccessDeniedException
] 和 {security-api-url}/org/springframework/security/core/AuthenticationException.html[AuthenticationException
] 转换为 HTTP 响应。
ExceptionTranslationFilter
作为 Security Filters 之一被插入到 FilterChainProxy 中。
下图显示了 ExceptionTranslationFilter
与其他组件之间的关系:
-
首先,
ExceptionTranslationFilter
调用FilterChain.doFilter(request, response)
以调用应用程序的其余部分。 -
如果用户未经身份验证或属于
AuthenticationException
,则 Start Authentication。-
HttpServletRequest
是 saved,以便在身份验证成功后可用于重放初始请求。 -
AuthenticationEntryPoint
用于从客户端请求凭据。例如,它可能重定向到登录页面或发送WWW-Authenticate
标头。
-
否则,如果是
AccessDeniedException
,则 Access Denied。调用AccessDeniedHandler
以处理拒绝访问。
如果应用程序未抛出 |
ExceptionTranslationFilter
的伪代码类似于以下内容:
try {
filterChain.doFilter(request, response); (1)
} catch (AccessDeniedException | AuthenticationException ex) {
if (!authenticated || ex instanceof AuthenticationException) {
startAuthentication(); (2)
} else {
accessDenied(); (3)
}
}
1 | 如 A Review of Filters 中所述,调用 FilterChain.doFilter(request, response) 等同于调用应用程序的其余部分。这意味着,如果应用程序的另一个部分 (<<`FilterSecurityInterceptor`,servlet-authorization-filtersecurityinterceptor>> 或方法安全性) 抛出一个 AuthenticationException 或 AccessDeniedException ,则在此处捕获并处理它。 |
2 | 如果用户未经身份验证或属于 AuthenticationException ,则 Start Authentication。 |
3 | Otherwise, Access Denied |
Saving Requests Between Authentication
如 Handling Security Exceptions 所示,当请求没有经过认证并且用于需要认证的资源时,需要保存针对经过认证资源的请求,以便在认证成功后重新请求。在 Spring Security 中,这是通过使用 <<`RequestCache`,requestcache>> 实现保存 HttpServletRequest
来完成的。
RequestCache
HttpServletRequest
保存到 {security-api-url}org/springframework/security/web/savedrequest/RequestCache.html[RequestCache
]。当用户成功进行身份验证时,使用 RequestCache
重播原始请求。<<`RequestCacheAwareFilter`,requestcacheawarefilter>> 便使用 RequestCache
来保存 HttpServletRequest
。
默认情况下,会使用 HttpSessionRequestCache
。下面的代码演示了如何自定义 RequestCache
实现,它用于在有 continue
参数时检查 HttpSession
是否有已保存请求。
@9
Prevent the Request From Being Saved
有许多原因可能导致你不希望将用户的未认证请求存储在会话中。你可能想将该存储卸载到用户的浏览器或者存储在数据库中。或者你可能想关闭此功能,因为你始终希望将用户重定向到主页,而不是登录前他们试图访问的页面。
为此,你可以使用 {security-api-url}org/springframework/security/web/savedrequest/NullRequestCache.html[实现 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"/>
Logging
Spring Security 提供全面记录所有与安全相关的事件,级别为 DEBUG 和 TRACE。这在调试应用程序时非常有用,因为出于安全措施,Spring Security 不会向响应正文中添加任何请求被拒绝的原因的详细信息。如果你遇到 401 或 403 错误,你很可能会找到一条日志消息,帮助你了解发生了什么。
我们来考虑一个示例,其中一个用户尝试向启用了 CSRF protection 的资源进行 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 令牌,这就是请求被拒绝的原因。
要配置应用程序以记录所有安全事件,可以将以下内容添加到应用程序中:
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>