乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Spring Security 基础

    • 本章定位
    • Spring Security 是什么
    • DelegatingFilterProxy
    • 安全过滤器链
    • SecurityFilterChain
    • 过滤器执行顺序
  • 第2章 认证

    • 本章定位
    • Authentication
    • 认证流程
    • AuthenticationManager
    • ProviderManager
    • DaoAuthenticationProvider
    • UserDetails
    • UserDetailsService
    • PasswordEncoder
    • BCryptPasswordEncoder
    • DelegatingPasswordEncoder
    • 表单登录
    • SecurityContext
    • SecurityContextHolder
    • UsernamePasswordAuthenticationToken
  • 第3章 授权

    • 本章定位
    • 授权模型
    • GrantedAuthority
    • AccessDecisionManager
    • AccessDecisionVoter
    • URL 级别授权
    • 方法级别安全
    • @PreAuthorize
    • @PostAuthorize
    • @PreFilter
    • @PostFilter
    • @Secured
    • RoleHierarchy
  • 第4章 过滤器链

    • 本章定位
    • FilterChainProxy
    • SecurityContextHolderFilter
    • LogoutFilter
    • BasicAuthenticationFilter
    • CsrfFilter
    • CorsFilter
    • HeaderWriterFilter
    • AnonymousAuthenticationFilter
    • RequestCacheAwareFilter
    • ExceptionTranslationFilter
    • FilterSecurityInterceptor
  • 第5章 会话管理

    • 本章定位
    • 会话管理
    • SessionFixation
    • 会话并发控制
    • SessionCreationPolicy
    • RememberMe
  • 第6章 JWT

    • 本章定位
    • JWT
    • JwtDecoder
    • JWT 认证
    • JwtAuthenticationConverter
  • 第7章 OAuth2

    • 本章定位
    • OAuth2 基础
    • OAuth2 Client
    • OAuth2 Resource Server
    • 第三方登录配置
  • 第8章 攻击防护

    • 本章定位
    • CSRF 跨站请求伪造防护
    • CORS 跨域防护
    • Clickjacking 点击劫持防护
    • 安全响应头
    • Session Fixation 会话固定防护
  • 第9章 测试

    • 本章定位
    • 安全测试
    • @WithMockUser
    • 最佳实践

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/usersapiSecurityHTTP Basic401 Unauthorized
/web/dashboardwebSecurity表单登录302 重定向到 /login
/public/logo.pngpublicSecurity无需认证直接访问
/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 失效。

正确做法

  1. 始终为 HttpSecurity 显式设置 securityMatcher,避免默认匹配全部。
  2. 使用 @Order 明确优先级,越精确的规则数字越小。
  3. 兜底链放最后,且确保其 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。因此配置顺序至关重要,精确匹配规则必须排在宽泛匹配规则之前。

上一页
本章定位
下一页
SecurityContextHolderFilter