参考:架构 :: Spring Security Reference (springdoc.cn)
一、过滤器
Spring Security 框架对 Servlet 请求的处理是基于过滤器机制。
容器会提前创建好FilterChain对每一个请求进行过滤,FilterChain中包含Filter 实例和 Servlet(Spring MVC应用程序中,是 DispatcherServlet 实例。)。
Filter对于一个请求,可以完成如下操作(一个 Filter 只会影响其下游的 Filter 实例和 Servlet,所以每个 Filter 的调用顺序是非常重要的。):
- 终止请求:Filter 通常会使用 HttpServletResponse 对客户端写入响应。
- 修改请求:修改下游的 Filter 实例和 Servlet 接收到的 HttpServletRequest 或 HttpServletResponse。
一个过滤器的过滤函数结构:
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
}
二、DelegatingFilterProxy
Servlet容器会使用自己的标准来注册 Filter 实例,这不同于Spring框架的IOC功能;也就是说,Servlet容器可能不会使用Spring的ApplicationContext(Bean容器)。
DelegatingFilterProxy的作用就是在 Servlet 容器的生命周期和 Spring 的 ApplicationContext 之间建立桥梁。
虽然Servlet容器不直接了解或管理由Spring框架定义的Bean,但是通过在Servlet容器中注册DelegatingFilterProxy这个过滤器(该过滤器会将所有的工作委托给Spring应用程序上下文中定义的具体的Filter实现的Spring Bean),过滤器的逻辑实际上是由Spring Bean来执行的,而不是由Servlet容器直接执行的。
DelegatingFilterProxy 从 ApplicationContext 查找过滤器的Bean,然后调用他们。
DelegatingFilterProxy的过滤函数伪代码:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
Filter delegate = getFilterBean(someBeanName);//获得一个过滤器的Bean
delegate.doFilter(request, response);//调用该过滤器
}
DelegatingFilterProxy还有一个优点,是允许延迟查找 Filter Bean实例:在Servlet容器启动之前,容器需要提前注册 Filter 实例;然而, Spring 通常使用 ContextLoaderListener 来加载 Spring Bean,这在Servlet容器注册完 Filter 实例之后才会完成,导致过滤器都是无法使用Bean容器中的Bean的(因为Bean还未创建,过滤器创建完成后内部的Bean引用都是空指针);现在通过DelegatingFilterProxy,就可以让Bean Filter使用Bean容器中的Bean了。
三、FilterChainProxy与SecurityFilterChain
FilterChainProxy 是一个 Bean,通常被包裹在 DelegatingFilterProxy 中。
FilterChainProxy 是 Spring Security 提供的一个特殊的 Filter,允许将请求通过 SecurityFilterChain 委托给许多 Filter 实例。
SecurityFilterChain 被 FilterChainProxy 用来确定当前请求应该调用哪些 Spring Security Filter 实例。
(可以将 FilterChainProxy 看做是 SecurityFilterChain 的带逻辑选择的hashset集合,而 SecurityFilterChain 又是 SecurityFilter 的顺序list集合。每个 SecurityFilterChain 都可以是唯一的,并且可以单独配置。)
单个Security Filter Chain示意图:
多个Security Filter Chain示意图:
在该多个Security Filter Chain的示意图中:
- 如果请求的URL是 /api/messages/,它首先与 /api/** 的 SecurityFilterChain0 模式匹配,所以只有 SecurityFilterChain_0 被调用,尽管它也与 SecurityFilterChain_n 匹配。
- 如果请求的URL是 /messages/,它与 /api/** 的 SecurityFilterChain_0 模式不匹配,所以 FilterChainProxy 继续尝试每个 SecurityFilterChain。假设没有其他 SecurityFilterChain 实例相匹配,则调用 SecurityFilterChain_n。
与直接向Servlet容器或 DelegatingFilterProxy 注册Security Filter 的 Bean 相比,向 FilterChainProxy 注册有很多优势:
- 提供起点进行故障诊断:FilterChainProxy 为 Spring Security 的所有 Servlet 支持提供了一个起点。如果试图对 Spring Security 的 Servlet 支持进行故障诊断,在 FilterChainProxy 中添加一个调试点是一个很好的开始。
- 执行一些关键任务:作为核心组件,FilterChainProxy 可以执行对应用程序的安全性和稳定性至关重要的任务。 例如,它清除了 SecurityContext 以避免内存泄漏。它还应用Spring Security的 HttpFirewall 来保护应用程序免受某些类型的攻击。
- 提供更大的灵活性:在Servlet容器中,Filter 实例仅基于URL被调用。 然而,FilterChainProxy 可以通过使用 RequestMatcher 接口,根据 HttpServletRequest 中的任何内容确定调用。
四、Security Filter
Security Filter 通过 SecurityFilterChain API 插入 FilterChainProxy 中。
举例:
@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();
}
}
过滤器顺序:
- CsrfFilter:HttpSecurity.csrf。CSRF过滤器,用于检查每个请求中是否存在CSRF令牌,并根据需要拦截或处理请求,以防止CSRF攻击。
- UsernamePasswordAuthenticationFilter:HttpSecurity.formLogin。表单登录过滤器,用于支持基于表单的用户身份验证,通常用于显示登录页面并接受用户凭据进行验证。
- BasicAuthenticationFilter:HttpSecurity.httpBasic。HTTP基本认证过滤器,用于支持基本的HTTP身份验证机制,当需要对请求进行身份验证时,它会提示用户提供用户名和密码进行认证。
- AuthorizationFilter:HttpSecurity.authorizeHttpRequests。授权过滤器用于验证对每个HTTP请求的访问权限,确保只有经过身份验证的用户才能访问受保护的资源。
可以看出过滤器顺序与声明顺序无关,具体的顺序由FilterOrderRegistration类决定。(源码为https://github.com/spring-projects/spring-security/tree/main/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java)
项目启动时会打印每个SecurityFilterChain所有的过滤器:
添加过滤器
自定义Filter的框架:
public class MyFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws Exception {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
if(XXX){
throw new AccessDeniedException("");
}else if(XXX){
throw new AuthenticationException("");
}
// do something before the rest of the application
filterChain.doFilter(request, response); // invoke the rest of the application
// do something after the rest of the application
}
}
在自定义一个过滤器后,可在注册SecurityFilterChain为Bean的函数中添加该过滤器,如:
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// ...
.addFilterBefore(new MyFilter(), AuthorizationFilter.class);
return http.build();
}
示例中的代码将自定义过滤器添加到 AuthorizationFilter 之前。也可以使用addFilterAfter 添加到某个特定的 filter 之后或使用 addFilterAt 将 filter 添加到 filter chain 中的某个位置。
注意:如果将过滤器声明为Bean,可能Springboot会自动将过滤器注册到Servlet容器中,导致一个请求会在Serlvet容器的过滤器中过滤一次,又在DelegatingFilterProxy中再过滤一次,为了避免这个问题,我们可以不使用@Bean、@Component,或声明 FilterRegistrationBean Bean 并将其 enabled 属性设置为 false 来告诉 Spring Boot 不要向容器注册它:
@Bean
public FilterRegistrationBean<TenantFilter> tenantFilterRegistration(TenantFilter filter) {
FilterRegistrationBean<MyFilter> registration = new FilterRegistrationBean<>(filter);
registration.setEnabled(false);
return registration;
}
五、ExceptionTranslationFilter:异常处理
ExceptionTranslationFilter 允许将 AccessDeniedException 和 AuthenticationException 翻译成 HTTP 响应。
ExceptionTranslationFilter 作为 Security Filter 之一被插入到 FilterChainProxy 中。
直接看不是很清晰,先看伪代码:
try {
filterChain.doFilter(request, response);
} catch (AccessDeniedException | AuthenticationException ex) {
if (!authenticated || ex instanceof AuthenticationException) {
startAuthentication();
} else {
accessDenied();
}
}
- 首先,ExceptionTranslationFilter 调用 FilterChain.doFilter(request, response) 来调用应用程序的其他部分。
- 如果用户没有被认证,或者是一个 AuthenticationException,那么就 开始认证(startAuthentication())。
- 否则,如果是 AccessDeniedException,那么就是 Access Denied(没有授权)。 AccessDeniedHandler 被调用来处理拒绝访问(access denied)。
六、RequestCache:保存认证之间的请求
在Spring Security中,请求缓存用于在用户进行身份验证之前保存用户的原始请求,以便在身份验证成功后将用户重定向回原始请求。这对于避免在登录后用户被重定向到应用程序的默认页面而失去原始请求非常有用。
当一个请求没有认证,会进入认证流程;当认证完成后,RequestCache 被用来重放原始请求。
默认情况下,使用一个 HttpSessionRequestCache 来保存 HttpServletRequest。
在SecurityFilterChain中定制RequestCache:
@Bean
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
requestCache.setMatchingRequestParameterName("continue");//自定义请求缓存的匹配参数名称,默认使用j_spring_security_request,Spring Security会将原始请求保存在名为 "continue" 的请求参数中。
http
// ...
.requestCache((cache) -> cache
.requestCache(requestCache)
);
return http.build();
}
关闭RequestCache:
RequestCache nullRequestCache = new NullRequestCache();