文章目录
- 过滤器回顾
- DelegatingFilterProxy
- FilterChainProxy
- SecurityFilterChain
- Security Filters
- 打印Security Filters
- 将自定义过滤器添加到过滤器链
- Handling Security Exceptions
- Saving Requests Between Authentication
- RequestCache
- Prevent the Request From Being Saved
- RequestCacheAwareFilter
Spring Security 是一个提供 身份验证(authentication)、 授权(authorization)和针对常见攻击的保护的框架。凭借对保护 Servlet应用程序和 Reactive应用程序的一流支持,它成为保护基于 Spring 的应用程序的事实上的标准。
Spring Security is a framework that provides authentication, authorization, and protection against common attacks. With first class support for securing both imperative and reactive applications, it is the de-facto standard for securing Spring-based applications.
本文主要介绍基于 Servlet 的应用程序中 Spring Security 的架构,不涉及基于 Reactive 应用程序的架构。
过滤器回顾
Spring Security 的 Servlet 支持是基于 Servlet Filter 的,因此首先大致了解一下 Filter 的作用是有帮助的。下图显示了单个 HTTP 请求处理的典型分层模型。
客户端向应用程序发起请求,Web 容器创建一个 FilterChain
,其中包含 Filter
实例和处理 HttpServletRequest
的 Servlet
的实例。在Spring MVC 应用程序中,这样的 Servlet
是 DispatcherServlet
实例。最多,一个 Servlet
可以处理一个 HttpServletRequest
和 HttpServletResponse
。但是,可以使用多个 Filter
用作:
- 阻止下游
Filter
实例或Servlet
实例被调用。在这种情况下,Filter
通常只处理HttpServletResponse
。 - 修改下游
Filter
实例和Servlet
实例使用的HttpServletRequest
或HttpServletResponse
。
Filter
的力量来自传递给它的 FilterChain
。
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
}
Servlet 和 Filter 的基础知识
由于 Filter
只影响下游的 Filter
实例和 Servlet
实例,因此调用每个 Filter
实例的顺序非常重要。
DelegatingFilterProxy
Spring提供了一个名为 DelegatingFilterProxy
的 Filter
实现,它允许在Servlet容器的生命周期和Spring的 ApplicationContext
之间进行桥接。Servlet容器允许使用自己的标准注册 Filter
实例,但它不知道Spring定义的Bean。您可以通过标准Servlet容器机制注册 DelegatingFilterProxy
,但将所有工作委托给实现 Filter
的Spring Bean。
下面是 DelegatingFilterProxy
在 FilterChain
中的位置:
DelegatingFilterProxy
从 ApplicationContext
查找Bean Filter 0 ,然后调用Bean Filter 0 。下面的清单显示了 DelegatingFilterProxy
的伪代码:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
Filter delegate = getFilterBean(someBeanName);
delegate.doFilter(request, response);
}
- 延迟获取注册为 Spring Bean 的 Filter。
DelegatingFilterProxy
中的delegate
是 Bean Filter 0 的实例。 - 将工作委托给 Spring Bean。
DelegatingFilterProxy
的另一个好处是它允许延迟查找 Filter
bean实例。这很重要,因为容器需要在启动之前注册 Filter
实例。然而,Spring通常使用 ContextLoaderListener
来加载Spring Bean,直到需要注册 Filter
实例之后才完成。
FilterChainProxy
Spring Security 的 Servlet 支持包含在 FilterChainProxy
中。 FilterChainProxy
是Spring Security提供的一个特殊的 Filter
,它允许通过 SecurityFilterChain
委托给许多 Filter
实例。由于 FilterChainProxy
是一个Bean,它通常被包装在DelegatingFilterProxy中。
下图显示了 FilterChainProxy
的角色。
SecurityFilterChain
SecurityFilterChain
被FilterChainProxy用来确定应该为当前请求调用哪些Spring Security Filter
实例。
下图显示了 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
可以通过使用RequestMatcher
接口来基于HttpServletRequest
中的任何内容确定调用。
下图显示了 Multiple SecurityFilterChain
实例:
在 Multiple 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
实例。
Security Filters
安全过滤器通过 SecurityFilterChain API 插入 FilterChainProxy。这些过滤器可以用于许多不同的目的,如身份验证、授权、漏洞利用保护等。过滤器以特定的顺序执行,以保证它们在正确的时间被调用,例如,执行身份验证的 Filter
应该在执行授权的 Filter
之前被调用。通常不需要知道 Spring Security 的 Filter
的顺序。然而,有时候知道排序是有益的,如果你想知道它们,你可以查看 org.springframework.security.config.annotation.web.builders.FilterOrderRegistration.java
代码。部分代码如下图:
打印Security Filters
在 WebSecurityConfig
配置类上修改@EnableWebSecurity
注解
@EnableWebSecurity(debug = true)
启动服务可以看到终端中有提示信息输出
前端请求接口,可以看到info信息
将自定义过滤器添加到过滤器链
-
实现 Filter 接口
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"); boolean hasAccess = isUserAllowed(tenantId); if (hasAccess) { filterChain.doFilter(request, response); return; } throw new AccessDeniedException("Access denied"); } }
-
将自定义 Filter 添加到 Security Filter Chain 中
@Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // ... .addFilterBefore(new TenantFilter(), AuthorizationFilter.class); return http.build(); }
通过在
AuthorizationFilter
之前添加过滤器,我们可以确保在身份验证过滤器之后调用TenantFilter
。您还可以使用HttpSecurity#addFilterAfter
将过滤器添加到特定过滤器之后,或使用HttpSecurity#addFilterAt
将过滤器添加到过滤器链中的特定过滤器位置。
如果您仍然想将 filter 声明为 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
ExceptionTranslationFilter
允许将 AccessDeniedException
和 AuthenticationException
转换为HTTP响应。
ExceptionTranslationFilter
作为安全过滤器之一插入FilterChainProxy。
下图显示了 ExceptionTranslationFilter
与其他组件的关系:
- 首先,
ExceptionTranslationFilter
调用FilterChain.doFilter(request, response)
应用程序的其余部分。 - 如果用户未经过身份验证或者是
AuthenticationException
,则开始身份验证。- SecurityContextHolder被清除。
HttpServletRequest
保存后,以便在身份验证成功后可用于重放原始请求。- 用于
AuthenticationEntryPoint
向客户端请求凭据。例如,它可能会重定向到登录页面或发送WWW-Authenticate
标头。
- 否则,如果它是
AccessDeniedException
,则Access Denied。调用AccessDeniedHandler
来处理拒绝访问。
如果应用程序不抛出 an
AccessDeniedException
或 anAuthenticationException
,ExceptionTranslationFilter
则不执行任何操作。
// 伪代码
try {
filterChain.doFilter(request, response);
} catch (AccessDeniedException | AuthenticationException ex) {
if (!authenticated || ex instanceof AuthenticationException) {
startAuthentication();
} else {
accessDenied();
}
}
Saving Requests Between Authentication
当请求没有身份验证并且是针对需要身份验证的资源时,需要保存已验证资源的请求,以便在身份验证成功后重新请求。在Spring Security中,这是通过使用 RequestCache
实现保存 HttpServletRequest
来完成的。
RequestCache
HttpServletRequest
保存在 RequestCache
中。当用户成功通过身份验证时,使用 RequestCache
来重放原始请求。 RequestCacheAwareFilter
是使用 RequestCache
来保存 HttpServletRequest
的。
默认情况下,使用 HttpSessionRequestCache
。下面的代码演示了如何自定义 RequestCache
实现,如果存在名为 continue
的参数,则该实现用于检查 HttpSession
是否已保存请求。
@Bean
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
requestCache.setMatchingRequestParameterName("continue");
http
// ...
.requestCache((cache) -> cache
.requestCache(requestCache)
);
return http.build();
}
Prevent the Request From Being Saved
您可能不希望在会话中存储用户的未经身份验证的请求,原因有很多。您可能希望将该存储卸载到用户的浏览器上或将其存储在数据库中。或者您可能希望关闭此功能,因为您总是希望将用户重定向到主页,而不是他们在登录前试图访问的页面。
要做到这一点,您可以使用 NullRequestCache
实现。
@Bean
SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
RequestCache nullRequestCache = new NullRequestCache();
http
// ...
.requestCache((cache) -> cache
.requestCache(nullRequestCache)
);
return http.build();
}
RequestCacheAwareFilter
RequestCacheAwareFilter
使用 RequestCache
来保存 HttpServletRequest
。