|
对于最新的稳定版本,请使用 Spring Security 6.5.3! |
架构
过滤器回顾
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抬头看豆过滤器0从ApplicationContext然后调用豆过滤器0.
以下列表显示了DelegatingFilterProxy:
DelegatingFilterProxy伪代码-
Java
-
Kotlin
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
// Lazily get Filter that was registered as a Spring Bean
// For the example in DelegatingFilterProxy delegate is an instance of Bean Filter0
Filter delegate = getFilterBean(someBeanName);
// delegate work to the Spring Bean
delegate.doFilter(request, response);
}
fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
// Lazily get Filter that was registered as a Spring Bean
// For the example in DelegatingFilterProxy delegate is an instance of Bean Filter0
val delegate: Filter = getFilterBean(someBeanName)
// delegate work to the Spring Bean
delegate.doFilter(request, response)
}
另一个好处DelegatingFilterProxy是它允许延迟查找Filterbean 实例。
这很重要,因为容器需要注册Filter实例,然后容器才能启动。
但是,Spring 通常使用ContextLoaderListener加载 Spring Beans,这要等到Filter实例需要注册。
过滤器链代理
Spring Security 的 Servlet 支持包含在FilterChainProxy.FilterChainProxy是一个特殊的Filter由 Spring Security 提供,允许委托给许多Filter实例通过SecurityFilterChain.
因为FilterChainProxy是一个 Bean,它通常包装在 DelegatingFilterProxy 中。
下图显示了FilterChainProxy.
安全过滤器链
SecurityFilterChainFilterChainProxy 使用哪个 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实例:
在 Multiple SecurityFilterChain 图中,FilterChainProxy决定哪个SecurityFilterChain应该使用。
只有第一个SecurityFilterChain调用匹配项。
如果 URL 为/api/messages/,它首先在SecurityFilterChain0模式/api/**,所以只有SecurityFilterChain0被调用,即使它也匹配SecurityFilterChainn.
如果 URL 为/messages/,则在SecurityFilterChain0模式/api/**所以FilterChainProxy继续尝试每个SecurityFilterChain.
假设没有其他SecurityFilterChain实例匹配,SecurityFilterChainn被调用。
请注意SecurityFilterChain0只有三个安全性Filter实例配置。
然而SecurityFilterChainn有四个安全性Filter实例配置。
需要注意的是,每个SecurityFilterChain可以是唯一的,并且可以单独配置。
事实上,一个SecurityFilterChain可能没有安全性Filter如果应用程序希望 Spring Security 忽略某些请求。
安全过滤器
安全过滤器使用 SecurityFilterChain API 插入到 FilterChainProxy 中。
这些过滤器可用于许多不同的目的,例如身份验证、授权、漏洞利用保护等。
过滤器按特定顺序执行,以保证在正确的时间调用它们,例如Filter执行身份验证的 应在Filter执行授权。
通常没有必要知道 Spring Security 的Filters.
但是,有时了解顺序是有益的,如果您想了解它们,可以查看FilterOrderRegistration法典.
为了举例说明上面的段落,让我们考虑以下安全配置:
@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 | 添加者 |
|---|---|
|
|
|
|
|
|
|
-
首先,
CsrfFilter被调用以防止 CSRF 攻击。 -
其次,调用身份验证过滤器来验证请求。
-
第三,该
AuthorizationFilter被调用以授权请求。
|
可能还有其他 |
打印安全过滤器
通常,查看安全列表很有用Filters 来调用特定请求。
例如,您要确保已添加的过滤器位于安全过滤器列表中。
筛选器列表在应用程序启动时以 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到安全过滤器链。
例如,假设您要添加一个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. |
|
而不是实施 |
现在,我们需要将过滤器添加到安全过滤器链中。
@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在过滤器链中的特定过滤器位置添加过滤器。
就是这样,现在TenantFilter将在筛选器链中调用,并将检查当前用户是否有权访问租户 ID。
将过滤器声明为 Spring Bean 时要小心,只需使用@Component或者通过在配置中将其声明为 bean,因为 Spring Boot 会自动将其注册到嵌入式容器中。
这可能会导致过滤器被调用两次,一次由容器调用,一次由 Spring Security 调用,并且以不同的顺序调用。
例如,如果您仍想将过滤器声明为 Spring bean 以利用依赖注入并避免重复调用,您可以通过声明FilterRegistrationBeanbean 并设置其enabled属性设置为false:
@Bean
public FilterRegistrationBean<TenantFilter> tenantFilterRegistration(TenantFilter filter) {
FilterRegistrationBean<TenantFilter> registration = new FilterRegistrationBean<>(filter);
registration.setEnabled(false);
return registration;
}
处理安全异常
这ExceptionTranslationFilter允许翻译AccessDeniedException和AuthenticationException转换为 HTTP 响应。
ExceptionTranslationFilter作为安全过滤器之一插入到 FilterChainProxy 中。
下图显示了ExceptionTranslationFilter到其他组件:
-
首先,ExceptionTranslationFilter调用FilterChain.doFilter(request, response)调用应用程序的其余部分。 -
如果用户未经过身份验证或是AuthenticationException,然后开始身份验证。-
这
HttpServletRequest保存,以便在身份验证成功后可用于重放原始请求。 -
这
AuthenticationEntryPoint用于向客户端请求凭据。例如,它可能会重定向到登录页面或发送WWW-Authenticate页眉。
-
否则,如果它是AccessDeniedException,然后拒绝访问。 这AccessDeniedHandler被调用来处理被拒绝的访问。
|
如果应用程序没有抛出 |
的伪代码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 | 否则,访问被拒绝 |
在身份验证之间保存请求
如处理安全异常中所示,当请求没有身份验证并且是针对需要身份验证的资源时,需要保存请求,以便经过身份验证的资源在身份验证成功后重新请求。在 Spring Security 中,这是通过保存HttpServletRequest使用RequestCache实现。
请求缓存
这HttpServletRequest保存在RequestCache. 当用户成功进行身份验证时,RequestCache用于重放原始请求。 这RequestCacheAwareFilter是使用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"/>
阻止保存请求
您可能希望不将用户未经身份验证的请求存储在会话中的原因有很多。 您可能希望将该存储卸载到用户的浏览器上或将其存储在数据库中。 或者您可能想关闭此功能,因为您总是希望将用户重定向到主页,而不是他们在登录前尝试访问的页面。
为此,您可以使用这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以保存HttpServletRequest.
Logging
Spring Security 在 DEBUG 和 TRACE 级别提供所有安全相关事件的全面日志记录。 这在调试应用程序时非常有用,因为对于安全措施,Spring Security 不会在响应正文中添加请求被拒绝原因的任何详细信息。 如果您遇到 401 或 403 错误,您很可能会找到一条日志消息,帮助您了解发生了什么。
让我们考虑一个示例,其中用户尝试将POST请求到启用了 CSRF 保护而不使用 CSRF Tokens的资源。
如果没有日志,用户将看到 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>