安全过滤器链
在 Spring Security 中,"安全过滤器链"(Security Filter Chain)不是指某一个具体的类,而是描述从 Servlet 容器到 Spring Security 核心过滤器之间完整的层级委托关系。理解这条链路的层级结构,是排查"为什么自定义过滤器不生效""为什么 Security 配置没拦截到请求"等问题的关键前提。
定义与作用:没有统一框架时的过滤器困境
在纯 Servlet 时代,开发者通过 web.xml 配置多个 <filter> 和 <filter-mapping> 来组成一条过滤器链。容器按照 filter-mapping 的声明顺序依次调用 doFilter。这种方式存在几个根本性问题:
- 生命周期割裂:Servlet Filter 由容器实例化,无法注入 Spring 管理的 Bean(如
UserDetailsService)。 - 顺序难以动态控制:不同模块各自声明过滤器,谁先谁后全凭
web.xml中的物理顺序,运行时无法根据路径灵活调整。 - 安全与业务耦合:登录校验、权限判断、日志记录、事务控制全部挤在一条链里,没有清晰的分层。
Spring Security 通过引入两层委托机制,将"Servlet 原生过滤器链"与"Spring Security 安全过滤器链"解耦:前者负责把请求桥接到 Spring 容器,后者负责按路径匹配不同的安全策略。
核心原理:三层过滤器架构
Spring Security 的过滤器链在物理上分为三个层级:
- Servlet 容器过滤器链(Container Filter Chain):由 Tomcat/Jetty 等容器维护,执行所有
javax.servlet.Filter。 - Spring 过滤器代理层(DelegatingFilterProxy → FilterChainProxy):
DelegatingFilterProxy是容器链中的一环,它将请求委托给 Spring 容器中的FilterChainProxyBean。 - Security 过滤器链(SecurityFilterChain):
FilterChainProxy内部维护多个SecurityFilterChain实例,每个实例包含一组有序的安全过滤器(如UsernamePasswordAuthenticationFilter、AuthorizationFilter等),并可根据请求路径独立匹配。
关键结论:
DelegatingFilterProxy只出现一次,固定在容器过滤器链中。FilterChainProxy只出现一次,作为 Spring Bean 存在,但它内部管理着多条SecurityFilterChain。- 请求到达时,
FilterChainProxy从上到下遍历所有SecurityFilterChain,第一个匹配当前请求路径的链被激活,执行其内部过滤器序列。 - 未匹配任何
SecurityFilterChain的请求,直接放行到后续 Servlet 过滤器或目标资源。
示例一:配置两条独立的安全过滤器链
场景说明
某应用同时提供 REST API(/api/**)和 Web 管理后台(/admin/**)。API 采用 HTTP Basic 认证且无 Session,后台采用表单登录且有 Session。两者需要完全不同的安全过滤器组合。
操作前配置:单链混用方案(混乱且不可维护)
如果所有请求共用一条 SecurityFilterChain,开发者不得不在同一条链里同时启用表单登录和 Basic 认证,导致未认证请求的行为不可预测:
@Bean
public SecurityFilterChain mixedChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.antMatchers("/api/**").authenticated()
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().permitAll()
)
.formLogin(Customizer.withDefaults()) // 对 API 也生效,不合适
.httpBasic(Customizer.withDefaults()); // 对后台也生效,冗余
return http.build();
}
问题分析:API 请求未携带认证信息时,UsernamePasswordAuthenticationFilter 会尝试将其重定向到 /login 页面,而不是返回 401,这不符合 REST API 的语义。
操作后配置:多链分离方案
@Configuration
@EnableWebSecurity
public class MultiChainSecurityConfig {
@Bean
@Order(1) // 数字越小优先级越高
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**") // 5.4+ 支持,5.x 也可用 requestMatcher
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain adminSecurityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/admin/**")
.authorizeHttpRequests(auth -> auth
.anyRequest().hasRole("ADMIN")
)
.formLogin(Customizer.withDefaults())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
);
return http.build();
}
@Bean
@Order(3)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.antMatchers("/", "/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
return http.build();
}
}
结果分析
| 请求路径 | 匹配的链 | 生效的认证方式 | Session 策略 |
|---|---|---|---|
/api/users | apiSecurityFilterChain | HTTP Basic | 无状态 |
/admin/dashboard | adminSecurityFilterChain | 表单登录 | 按需创建 |
/home | defaultSecurityFilterChain | 表单登录 | 按需创建 |
- 通过
@Order控制链的匹配优先级,避免宽泛的/**被提前匹配而覆盖后续规则。 securityMatcher让每条链只关注自己的路径范围,过滤器组合互不干扰。
示例二:自定义过滤器插入安全过滤器链
场景说明
需要在所有请求进入认证和授权逻辑之前,先记录一条安全审计日志(如记录请求来源 IP 和访问目标)。该日志组件需要注入 Spring 的 HttpServletRequest 工具类,因此不能注册为原生 Servlet Filter(否则生命周期在容器层,无法享受 Spring DI)。
操作前配置:错误地注册为 Servlet Filter
@Component
@WebFilter(urlPatterns = "/*") // 错误!这会注册到 Servlet 容器,而非 Security 链
public class AuditLogFilter implements Filter {
@Autowired
private AuditLogService auditLogService; // 可能注入失败或延迟
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
auditLogService.log(((HttpServletRequest) req).getRequestURI());
chain.doFilter(req, res);
}
}
问题分析:@WebFilter 让 Servlet 容器在 Spring 启动完成前就实例化过滤器,此时 AuditLogService 可能尚未就绪;且该过滤器独立于 Spring Security 链,无法精确控制它在 Security 上下文加载之前还是之后执行。
操作后配置:将自定义过滤器加入 SecurityFilterChain
public class AuditLogFilter extends OncePerRequestFilter {
@Autowired
private AuditLogService auditLogService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
auditLogService.log(request.getRemoteAddr(), request.getRequestURI());
filterChain.doFilter(request, response);
}
}
在配置类中将其插入 SecurityFilterChain:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private AuditLogFilter auditLogFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.addFilterBefore(auditLogFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests(auth -> auth
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
return http.build();
}
}
结果分析
- 使用
addFilterBefore将AuditLogFilter插入到UsernamePasswordAuthenticationFilter之前,保证审计日志在认证逻辑之前记录。 - 由于
AuditLogFilter是 Spring Bean,可以正常注入AuditLogService和其他依赖。 - 该过滤器只在
SecurityFilterChain生效的请求中执行,不会干扰静态资源或未被 Security 拦截的路径。
易错场景与面试考点
易错场景:自定义过滤器被容器和 Spring Security 各执行一次
很多开发者在自定义过滤器上同时加了 @Component(或 @WebFilter)和 addFilterBefore,导致过滤器在容器过滤器链和 Security 过滤器链中各出现一次,请求被处理两次。
错误代码:
@Component // 被 Spring Boot 自动扫描并注册到 Servlet 容器
public class MyFilter extends OncePerRequestFilter { ... }
// 配置类中又手动加入 Security 链
http.addFilterBefore(myFilter, UsernamePasswordAuthenticationFilter.class);
正确做法:
// 1. 如果走 Security 链,则不要加 @WebFilter,也不要让 Spring Boot 自动注册为 Servlet Filter
// 2. 仅通过配置类加入 SecurityFilterChain
public class MyFilter extends OncePerRequestFilter { ... }
// 配置类
http.addFilterBefore(new MyFilter(), UsernamePasswordAuthenticationFilter.class);
// 或者如果 MyFilter 需要注入依赖,可以声明为 @Component,但务必确认没有 @WebFilter
面试考点:面试官常问 "Spring Security 的过滤器链与 Servlet 过滤器链有什么区别?" 回答要点:
- Servlet 过滤器链由容器管理,按
web.xml或@WebFilter顺序执行;Spring Security 的过滤器链由FilterChainProxy管理,按SecurityFilterChain内部顺序执行。DelegatingFilterProxy是两者的唯一交汇点:它是 Servlet 链中的一环,负责将请求委托给 Spring 容器中的FilterChainProxy。- 多个
SecurityFilterChain可以共存,按@Order和securityMatcher匹配;而 Servlet 过滤器链没有这种按路径动态分支的机制。
版本说明:本章节示例基于 Spring Security 5.x 语法,使用
securityMatcher、antMatchers以及addFilterBefore等 5.x 典型配置风格。