FilterChainProxy
定义与作用
FilterChainProxy 是 Spring Security 的核心过滤器 Bean,以 springSecurityFilterChain 为名称注册在 Spring 容器中。它的职责是管理多个 SecurityFilterChain,根据请求路径匹配规则,将每个请求路由到唯一对应的过滤器链执行。它是 DelegatingFilterProxy 的委托目标,也是所有 Spring Security 过滤器的统一入口。
核心原理
1. 多链匹配机制
FilterChainProxy 内部维护 List<SecurityFilterChain>,请求到达时按声明顺序遍历,调用 SecurityFilterChain.matches(request) 判断:
- 第一个匹配生效:一旦匹配成功,立即使用该链处理请求,后续链不再参与。
- 未匹配则放行:如果所有链都不匹配,请求直接走原始 Servlet FilterChain,Spring Security 不介入。
// FilterChainProxy 核心逻辑(简化)
public void doFilter(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) {
// 1. 遍历所有 SecurityFilterChain
for (SecurityFilterChain securityFilterChain : filterChains) {
if (securityFilterChain.matches(request)) {
// 2. 第一个匹配者生效,执行其内部过滤器列表
doFilterInternal(request, response, securityFilterChain);
return;
}
}
// 3. 未匹配,放行到原始 FilterChain
chain.doFilter(request, response);
}
2. 虚拟过滤器链(VirtualFilterChain)
FilterChainProxy 不会将请求直接交给原生的 FilterChain 和 SecurityFilterChain 混合执行。它内部创建虚拟过滤器链,将当前匹配的 SecurityFilterChain 中的过滤器逐一执行,全部完成后才回退到原始 Servlet FilterChain 的后续过滤器。
完整示例一:多 SecurityFilterChain 隔离 API 与 Web 页面
场景说明
一个应用同时提供:
- Web 前端页面(
/web/**):使用表单登录 + Session。 - REST API 接口(
/api/**):使用 HTTP Basic + 无状态。 - 公共资源(
/public/**):无需认证,直接访问。
操作前配置(单链,所有请求统一处理)
// 问题:单链下 API 和 Web 共享同一套认证规则,导致冲突
@Bean
public SecurityFilterChain singleChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults()) // 对 API 也启用表单登录,不合理
.httpBasic(Customizer.withDefaults()); // 对 Web 页面也启用 Basic,不友好
return http.build();
}
问题分析:单链导致 /api/** 在认证失败时也会返回 302 重定向到登录页,这对 API 客户端极不友好,应返回 401。
操作后配置(多链,精确匹配)
@Configuration
@EnableWebSecurity
public class MultiChainSecurityConfig {
// 链1:API 接口,优先声明(匹配 /api/** 的请求)
@Bean
@Order(1) // 数字越小优先级越高
public SecurityFilterChain apiSecurity(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**") // 仅匹配 /api/**
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults()) // 仅 API 使用 Basic
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
); // API 无状态,不创建 Session
return http.build();
}
// 链2:Web 页面
@Bean
@Order(2)
public SecurityFilterChain webSecurity(HttpSecurity http) throws Exception {
http
.securityMatcher("/web/**")
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults()) // Web 使用表单登录
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
);
return http.build();
}
// 链3:公共资源(兜底)
@Bean
@Order(3)
public SecurityFilterChain publicSecurity(HttpSecurity http) throws Exception {
http
.securityMatcher("/public/**")
.authorizeHttpRequests(auth -> auth
.anyRequest().permitAll()
);
return http.build();
}
}
结果分析
| 请求路径 | 匹配到的 SecurityFilterChain | 认证方式 | 未认证时的响应 |
|---|---|---|---|
/api/users | apiSecurity | HTTP Basic | 401 Unauthorized |
/web/dashboard | webSecurity | 表单登录 | 302 重定向到 /login |
/public/logo.png | publicSecurity | 无需认证 | 直接访问 |
/other/path | 无匹配 | — | 直接放行(Spring Security 不介入) |
完整示例二:精确匹配与顺序冲突
场景说明
开发者先配置了一个宽泛的 /** 匹配链,后配置了一个更精确的 /api/** 链。由于 FilterChainProxy 按声明顺序匹配,/api/** 永远被跳过。
操作前配置(顺序错误)
@Configuration
public class WrongOrderConfig {
@Bean
@Order(1)
public SecurityFilterChain defaultSecurity(HttpSecurity http) throws Exception {
http
.securityMatcher("/**") // 匹配所有请求
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain apiSecurity(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**") // 永远匹配不到!
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults());
return http.build();
}
}
问题分析:/api/** 的请求先被 defaultSecurity 的 /** 匹配,永远使用表单登录规则。apiSecurity 的 httpBasic 和 STATELESS 配置完全失效。
操作后配置(修正顺序)
@Configuration
public class CorrectOrderConfig {
@Bean
@Order(1)
public SecurityFilterChain apiSecurity(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**") // 精确的放前面
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain defaultSecurity(HttpSecurity http) throws Exception {
http
.securityMatcher("/**") // 宽泛的兜底放后面
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
return http.build();
}
}
结果分析
修正后:
/api/users被apiSecurity匹配,使用 HTTP Basic,无状态。/web/page被defaultSecurity匹配,使用表单登录。/api开头的所有请求不再受/**链影响。
易错场景:忽略 securityMatcher 导致默认链覆盖所有请求
错误写法
@Bean
public SecurityFilterChain defaultSecurity(HttpSecurity http) throws Exception {
http
// 未写 .securityMatcher(...) 时,默认匹配所有请求
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
return http.build();
}
@Bean
public SecurityFilterChain apiSecurity(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults());
return http.build();
}
陷阱:当多个 SecurityFilterChain Bean 都不加 @Order 时,Spring 按 Bean 定义顺序加载。如果 defaultSecurity 先被定义(没有 securityMatcher 也默认匹配 /**),它会覆盖所有请求,导致 apiSecurity 失效。
正确做法
- 始终为
HttpSecurity显式设置securityMatcher,避免默认匹配全部。 - 使用
@Order明确优先级,越精确的规则数字越小。 - 兜底链放最后,且确保其
securityMatcher不会吞掉其他链的匹配范围。
// 兜底链应明确排除已被其他链覆盖的路径
@Bean
@Order(99)
public SecurityFilterChain fallbackSecurity(HttpSecurity http) throws Exception {
http
.securityMatchers(matchers -> matchers
.requestMatchers("/**") // 兜底
.requestMatchers("/api/**").not() // 排除 /api/**(注意:5.x 写法不同,建议直接调整顺序)
)
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
return http.build();
}
面试考点:
FilterChainProxy如何确定使用哪个SecurityFilterChain? 答:按声明顺序(配合@Order)遍历filterChains列表,调用matches(request),第一个返回true的链生效。未匹配则直接放行到原始 Servlet FilterChain。因此配置顺序至关重要,精确匹配规则必须排在宽泛匹配规则之前。