|
此版本仍在开发中,尚未被视为稳定版本。对于最新的稳定版本,请使用 Spring Security 6.4.1! |
架构
本节讨论 Spring Security 在基于 Servlet 的应用程序中的高级体系结构。 我们在参考的 Authentication, Authorization, and Protection against Exploits 部分中建立了这种高级理解。
过滤器回顾
Spring Security 的 Servlet 支持基于 Servlet 过滤器,因此通常首先查看过滤器的角色会很有帮助。 下图显示了单个 HTTP 请求的处理程序的典型分层。
客户端向应用程序发送请求,容器创建一个FilterChain,其中包含Filterinstances 和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查找Bean 过滤器0从ApplicationContext然后调用Bean 过滤器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 的 Filter。
对于 DelegatingFilterProxy 中的示例 delegate是Bean 过滤器0. |
| 2 | 将工作委托给 Spring Bean。 |
另一个好处DelegatingFilterProxy是它允许延迟查找Filterbean 实例。
这很重要,因为容器需要注册Filter实例。
但是, Spring 通常使用ContextLoaderListener来加载 Spring Bean,这只有在Filter需要注册实例。
FilterChainProxy
Spring Security 的 Servlet 支持包含在FilterChainProxy.FilterChainProxy是一种特殊的Filter由 Spring Security 提供,允许委托给多个Filter实例SecurityFilterChain.
因为FilterChainProxy是一个 Bean,它通常包装在 DelegatingFilterProxy 中。
下图显示了FilterChainProxy.
SecurityFilterChain 安全过滤器链
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实例:
在多个 SecurityFilterChain 图中,FilterChainProxy决定哪个SecurityFilterChain应该使用。
只有第一个SecurityFilterChainthat matches 被调用。
如果 URL 为/api/messages/请求时,它首先匹配SecurityFilterChain0的模式/api/**,所以只有SecurityFilterChain0被调用,即使它也匹配SecurityFilterChainn.
如果 URL 为/messages/请求时,它与SecurityFilterChain0的模式/api/**所以FilterChainProxy继续尝试每个SecurityFilterChain.
假设没有其他SecurityFilterChaininstances match、SecurityFilterChainn被调用。
请注意,SecurityFilterChain0只有三个安全Filter已配置实例。
然而SecurityFilterChainn有四项安全保障Filter已配置实例。
需要注意的是,每个SecurityFilterChain可以是唯一的,并且可以单独配置。
实际上,SecurityFilterChain可能具有零安全性Filter实例(如果应用程序希望 Spring Security 忽略某些请求)。
安全过滤器
安全筛选器使用 SecurityFilterChain API 插入到 FilterChainProxy 中。
这些过滤器可用于许多不同的目的,例如漏洞利用保护、身份验证、授权等。
过滤器按特定顺序执行,以保证它们在正确的时间被调用,例如,Filter执行身份验证的Filter执行授权。
通常不需要知道 Spring Security 的Filters.
但是,有时了解顺序是有益的,如果您想了解它们,可以检查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订购:
| Filter | 添加者 |
|---|---|
|
|
|
|
|
|
|
-
首先,
CsrfFilter用于防止 CSRF 攻击。 -
其次,调用身份验证筛选器来验证请求。
-
第三这
AuthorizationFilter用于授权请求。
|
可能还有其他 |
打印安全过滤器
通常,查看安全列表很有用Filter为特定请求调用的 s。
例如,您希望确保已添加的过滤器位于安全过滤器列表中。
筛选器列表在应用程序启动时以 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 足以为您的应用程序提供安全性。
但是,有时您可能希望添加自定义Filter添加到 SecurityFilterChain 中。
HttpSecurity提供三种添加过滤器的方法:
-
#addFilterBefore(Filter, Class<?>)在另一个过滤器之前添加您的过滤器 -
#addFilterAfter(Filter, Class<?>)在另一个过滤器之后添加您的过滤器 -
#addFilterAt(Filter, Class<?>)将另一个过滤器替换为您的过滤器
添加自定义过滤器
如果要创建自己的过滤器,则需要确定它在过滤器链中的位置。 请查看筛选条件链中发生的以下关键事件:
考虑您需要发生哪些事件才能找到您的过滤器。 以下是经验法则:
| 如果您的过滤器是 a(n) | 然后把它放在 | 由于这些事件已经发生 |
|---|---|---|
Exploit Protection 筛选器 |
SecurityContextHolderFilter |
1 |
身份验证筛选器 |
LogoutFilter |
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。
将 Filter 声明为 Bean
当您声明Filter作为 Spring Bean 中,要么用@Component或者通过在配置中将其声明为 bean,Spring Boot 会自动将其注册到嵌入式容器中。
这可能会导致过滤器被调用两次,一次由容器调用,一次由 Spring Security 调用,并且顺序不同。
因此,过滤器通常不是 Spring bean。
但是,如果你的过滤器需要是一个 Spring Bean(例如,为了利用依赖注入),你可以通过声明一个FilterRegistrationBeanbean 并将其enabledproperty 设置为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 过滤器,请使用 DSLaddFilterAt这样:
-
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作为安全筛选器之一插入到 FilterChainProxy 中。
下图显示了ExceptionTranslationFilter到其他组件:
-
首先,ExceptionTranslationFilter调用FilterChain.doFilter(request, response)以调用应用程序的其余部分。 -
如果用户未经过身份验证或用户是AuthenticationException,然后单击 Start Authentication (开始身份验证)。-
这
HttpServletRequest保存,以便在身份验证成功后可用于重放原始请求。 -
这
AuthenticationEntryPoint用于向客户端请求凭据。 例如,它可能会重定向到登录页面或发送WWW-Authenticate页眉。
-
否则,如果它是AccessDeniedException,然后单击 Access Denied。 这AccessDeniedHandler用于处理 Access Denied。
|
如果应用程序没有抛出 |
的伪代码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 | 否则,Access Denied (访问被拒绝) |
在身份验证之间保存请求
如处理安全异常中所述,当请求没有身份验证并且针对需要身份验证的资源时,需要保存请求,以便经过身份验证的资源在身份验证成功后重新请求。
在 Spring Security 中,这是通过保存HttpServletRequest使用RequestCache实现。
请求缓存
这HttpServletRequest保存在RequestCache.
当用户成功进行身份验证时,RequestCache用于重放原始请求。
这RequestCacheAwareFilter使用RequestCache获取保存的HttpServletRequest在用户进行身份验证后,虽然ExceptionTranslationFilter使用RequestCache以保存HttpServletRequest检测到AuthenticationException,然后再将用户重定向到登录终端节点。
默认情况下,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 错误,您很可能会找到一条日志消息,帮助您了解发生了什么。
让我们考虑一个用户尝试将POST请求到启用了 CSRF 保护但未启用 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>