
Spring Security 利用 Servlet Filter 实现:

主要的组件:
-
DelegatingFilterProxy:从 Spring 容器中加载 Filter 实例并调用
-
FilterChainProxy:特殊的 Filter,用于调用多个 SecurityFilterChain
-
SecurityFilterChain:SecurityFilter 的调用链,可以包含多个以对应不同的路径规则,执行时会根据请求路径依次匹配,执行匹配到的第一个调用链。
SecurityFilterChain 可以包含 0 个 SecurityFilter,比如如果希望程序忽略某个路径(不做处理),可以为其添加一个包含 0 个 SecurityFilter 的 SecurityFilterChain。
Security Filter
Security Filter 用于执行具体的调用链行为,最常见的是进行身份认证或授权。需要注意的是它们在调用链上的顺序相当关键,比如用于认证的 Filter 必须在用于授权的 Filter 之前,这点和 Servlet 的 Filter 是一样的。
通常使用HttpSecurity添加并构建一个SecurityFilterChain:
public class SecurityConfig {
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) {
httpSecurity.csrf(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults())
.formLogin(Customizer.withDefaults())
.authorizeHttpRequests(authorize ->
authorize.anyRequest().authenticated());
return httpSecurity.build();
}
}
这里依次添加了三个过滤器:
-
CsrfFilter:防止 CSRF 攻击
-
认证过滤器:用于身份认证
-
授权过滤器:用于对请求进行授权
打印过滤器链
可以添加配置以打印 Spring Security 日志:
# 日志级别设置为 debug
logging.level.org.springframework.security=debug
打印的日志:
2025-12-25T10:09:47.066+08:00 DEBUG 20476 --- [demo] [ main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with filters: DisableEncodeUrlFilter, WebAsyncManagerIntegrationFilter, SecurityContextHolderFilter, HeaderWriterFilter, CsrfFilter, LogoutFilter, UsernamePasswordAuthenticationFilter, DefaultResourcesFilter, DefaultLoginPageGeneratingFilter, DefaultLogoutPageGeneratingFilter, BasicAuthenticationFilter, RequestCacheAwareFilter, SecurityContextHolderAwareRequestFilter, AnonymousAuthenticationFilter, TenantFilter, ExceptionTranslationFilter, AuthorizationFilter
这里边是加载的 SecurityFitlerChain 中的 Security Filter 信息,可以从这里边看到 Filter 的先后顺序。
向过滤器链添加过滤器
HttpSecurity 提供了三种添加过滤器的方法:
-
#addFilterBefore(Filter, Class<?>)在另一个过滤器之前添加你的过滤器 -
#addFilterAfter(Filter, Class<?>)在另一个过滤器之后添加你的过滤器 -
#addFilterAt(Filter, Class<?>)用你的过滤器替换另一个过滤器
假设需要添加一个检查当前用户是否可以访问特定租户信息的 Filter(多租户系统):
public class TenantFilter implements Filter {
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");
}
private boolean isUserAllowed(String tenantId) {
// 模拟检查用户是否允许访问租户
return tenantId != null && !tenantId.isEmpty();
}
}
这里通过请求头信息获取租户编码,然后检查当前用户是否可以访问,如果不行,就抛出异常。
现在添加这个 Filter 到过滤器链,因为需要获取当前用户信息,所以这个过滤器应当在认证过滤器之后:
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) {
// ...
httpSecurity.addFilterAfter(new TenantFilter(), AnonymousAuthenticationFilter.class);
return httpSecurity.build();
}
声明过滤器为 Spring Bean
通常不应当让 Security Filter 作为 Spring Bean 存在,因为这样做可能让 Filter 被调用两次,一次是 Spring Security 调用,一次是 Spring 容器调用。
如果一定要这么做(比如需要以依赖注入的方式使用 Filter),需要添加特殊设置,以告诉 Spring 容器这个 Filter 不应当被注册到容器:
public FilterRegistrationBean<TenantFilter> tenantFilterRegistration(TenantFilter tenantFilter) {
FilterRegistrationBean<TenantFilter> registration = new FilterRegistrationBean<>(tenantFilter);
registration.setEnabled(false);
return registration;
}
public class TenantFilter implements Filter {
// ...
}
这样就只有 Spring Security 会使用这个 Bean 作为 Filter。
自定义 Spring Security 过滤器
通常,会使用链式调用(DSL)的方式添加过滤器:
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity, TenantFilter tenantFilter) {
httpSecurity.csrf(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults())
.formLogin(Customizer.withDefaults())
.authorizeHttpRequests(authorize ->
authorize.anyRequest().authenticated());
httpSecurity.addFilterAfter(tenantFilter, AnonymousAuthenticationFilter.class);
return httpSecurity.build();
}
如果要用自定义 Filter 取代原有的 Filter,可以:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity,
TenantFilter tenantFilter,
AuthenticationManager authManager
) {
httpSecurity.csrf(Customizer.withDefaults())
.formLogin(Customizer.withDefaults())
.authorizeHttpRequests(authorize ->
authorize.anyRequest().authenticated());
httpSecurity.addFilterAfter(tenantFilter, AnonymousAuthenticationFilter.class);
BasicAuthenticationFilter basicAuthenticationFilter = new BasicAuthenticationFilter(authManager);
httpSecurity.addFilterAt(basicAuthenticationFilter, BasicAuthenticationFilter.class);
return httpSecurity.build();
}
同一个 Filter 只能在 FilterChain 上添加一次,如果重复添加可能报错:
httpSecurity.csrf(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults())
.formLogin(Customizer.withDefaults())
.authorizeHttpRequests(authorize ->
authorize.anyRequest().authenticated());
httpSecurity.addFilterAfter(tenantFilter, AnonymousAuthenticationFilter.class);
BasicAuthenticationFilter basicAuthenticationFilter = new BasicAuthenticationFilter(authManager);
httpSecurity.addFilterAt(basicAuthenticationFilter, BasicAuthenticationFilter.class);
此时可以移除原来的 Filter 添加,或者利用 DSL 关闭 Filter:
httpSecurity.httpBasic(AbstractHttpConfigurer::disable);
httpSecurity.addFilterAt(basicAuthenticationFilter, BasicAuthenticationFilter.class);
处理安全异常
查看日志可以看到AuthorizationFilter过滤器前有一个ExceptionTranslationFilter过滤器。该过滤器可以捕获其后产生的AccessDeniedException或AuthenticationException异常。
如果是未登录或AuthenticationException异常,会重定向到登录页面进行登录,如果是AccessDeniedException异常,访问会被拒绝,AccessDeniedHandler会被调用。
AuthorizationFilter中的伪代码如下:
try {
filterChain.doFilter(request, response);
} catch (AccessDeniedException | AuthenticationException ex) {
if (!authenticated || ex instanceof AuthenticationException) {
startAuthentication();
} else {
accessDenied();
}
}
保存认证前的请求
当一个请求没有认证信息且需要访问需要认证的资源时,需要保存该请求以便在认证成功后重新发起。在 Spring Security 中,这是通过使用 HttpServletRequest 来实现的,具体由 RequestCache 实现。
请求缓存
举例说明,比如我们需要缓存请求参数中有continue的 HTTP 请求,以便在用户认证后再次执行该请求:
@Bean
public DefaultSecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
requestCache.setMatchingRequestParameterName("continue");
httpSecurity.requestCache(cache -> cache.requestCache(requestCache));
return httpSecurity.build();
}
防止请求被缓存
如果不想请求被缓存,即禁用请求缓存功能,可以:
@Bean
public SecurityFilterChain securityFilterChain2(HttpSecurity httpSecurity) throws Exception {
RequestCache requestCache = new NullRequestCache();
httpSecurity.requestCache(cache -> cache.requestCache(requestCache));
return httpSecurity.build();
}
RequestCacheAwareFilter
该过滤器使用 RequestCache 来重放原始请求。
日志记录
Spring Security 提供了在 DEBUG 和 TRACE 级别对所有与安全相关的事件的全面日志记录。这在调试应用程序时非常有用,因为 Spring Security 不会在响应体中添加任何关于请求被拒绝原因的详细信息。如果你遇到 401 或 403 错误,很可能你会发现一条有助于理解问题的日志信息。
可以通过修改配置启用日志:
logging.level.org.springframework.security=TRACE

文章评论