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

    • 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
    • 最佳实践

过滤器执行顺序

Spring Security 的安全过滤器链内部由一组按固定顺序排列的核心过滤器组成。每个过滤器在特定的时机执行特定的职责:有的负责加载安全上下文,有的负责认证,有的负责授权,有的负责异常转译。理解它们的执行顺序和职责边界,是定位"为什么在自定义过滤器里获取不到当前用户""为什么授权判断先于认证完成"等诡异问题的关键。


定义与作用:顺序即契约

在没有安全框架时,开发者可能会编写多个独立的过滤器:一个做登录校验,一个做权限判断,一个做日志记录。这些过滤器的执行顺序通常由 web.xml 中 <filter-mapping> 的物理顺序决定。这种机制存在严重隐患:

  • 顺序不可预测:如果两个过滤器的映射顺序被无意中调换,权限判断可能在登录校验之前执行,导致永远鉴权失败。
  • 职责重叠:每个过滤器都自行从 Session 中读取用户状态,没有统一的安全上下文抽象。
  • 异常处理不统一:认证失败和权限不足分别由不同的过滤器抛出不同异常,没有统一的转译机制。

Spring Security 将安全过滤器视为一条精密流水线,每个位置都有固定的职责。顺序不是随意的,而是硬编码在 FilterOrderRegistration 中的契约。例如,SecurityContextHolderFilter 必须在 UsernamePasswordAuthenticationFilter 之前,因为后者依赖前者加载或初始化 SecurityContext;ExceptionTranslationFilter 必须在 AuthorizationFilter 之前,因为它需要捕获后者抛出的 AccessDeniedException。


核心原理:固定顺序与职责分工

Spring Security 5.x 中核心安全过滤器的标准执行顺序如下:

顺序过滤器核心职责
1DisableEncodeUrlFilter禁止响应 URL 中携带 jsessionid,防止 Session ID 泄露
2WebAsyncManagerIntegrationFilter将 SecurityContext 绑定到异步请求(WebAsyncManager)
3SecurityContextHolderFilter / SecurityContextPersistenceFilter从 Session 或请求中加载 SecurityContext,绑定到当前线程
4HeaderWriterFilter写入安全响应头(X-Frame-Options、X-Content-Type-Options 等)
5CorsFilter处理跨域预检请求(OPTIONS)和实际跨域响应头
6CsrfFilter生成和校验 CSRF Token,防御跨站请求伪造
7LogoutFilter处理 /logout 请求,销毁 Session、清除 Cookie、调用登出处理器
8UsernamePasswordAuthenticationFilter拦截 POST /login,执行表单登录认证
9BasicAuthenticationFilter解析 Authorization: Basic ... 请求头,执行 HTTP Basic 认证
10RequestCacheAwareFilter恢复被登录中断前的原始请求(如访问受保护页面被重定向到登录页后)
11SecurityContextHolderAwareRequestFilter包装 HttpServletRequest,使其支持 HttpServletRequest.getUserPrincipal() 等 Servlet 3.0 安全方法
12AnonymousAuthenticationFilter如果当前请求仍未认证,为其填充一个匿名 Authentication(anonymousUser,权限 ROLE_ANONYMOUS)
13ExceptionTranslationFilter捕获下游过滤器抛出的 AuthenticationException(转译为 401/重定向登录)和 AccessDeniedException(转译为 403)
14AuthorizationFilter / FilterSecurityInterceptor执行最终的访问控制决策,检查当前用户是否具备请求资源所需的权限

关键结论:

  • 顺序是固定的,由 FilterOrderRegistration 类硬编码。开发者自定义的过滤器只能插入到某个已有过滤器之前或之后,不能改变核心过滤器之间的相对顺序。
  • SecurityContextHolderFilter 尽早执行,确保后续所有过滤器都能通过 SecurityContextHolder.getContext() 获取当前认证信息。
  • AnonymousAuthenticationFilter 在认证过滤器之后,这意味着:如果用户未登录,认证过滤器不会报错,而是让匿名过滤器为其填充一个匿名身份,保证 SecurityContext 不为 null。
  • ExceptionTranslationFilter 必须位于 AuthorizationFilter 之前,它是整条链的"异常捕手"。
  • AuthorizationFilter 位于链尾,是请求能否访问资源的最终闸口。

示例一:观察并验证过滤器的实际执行顺序

场景说明

在一个 Spring Boot + Spring Security 5.x 项目中,开启 TRACE 级日志,观察某次表单登录请求经过的过滤器序列,验证理论顺序与实际情况一致。

操作前配置:默认日志级别,看不到过滤器细节

默认 application.properties:

# 默认日志级别为 INFO,看不到 Security 过滤器细节
logging.level.org.springframework.security=INFO

启动后访问 /login 并提交表单,控制台仅输出:

o.s.s.w.a.UsernamePasswordAuthenticationFilter : Authentication attempt ...
o.s.s.w.a.AbstractAuthenticationProvider       : Authenticated user admin

看不到过滤器的进入和退出顺序,排查问题时难以定位请求卡在哪一环。

操作后配置:开启 TRACE 日志观察完整链路

# application.properties
logging.level.org.springframework.security=TRACE
logging.level.org.springframework.security.web=TRACE

重新启动并访问 /login,控制台将输出(节选关键 TRACE 日志):

TRACE o.s.s.w.DisableEncodeUrlFilter           : Filter 'DisableEncodeUrlFilter' ...
TRACE o.s.s.w.c.SecurityContextHolderFilter      : Created SecurityContextHolderFilter
TRACE o.s.s.w.h.HeaderWriterFilter               : HeaderWriterFilter generated header ...
TRACE o.s.s.w.c.CsrfFilter                       : Validated CSRF token
TRACE o.s.s.w.a.u.UsernamePasswordAuthenticationFilter : Set SecurityContextHolder to ...
TRACE o.s.s.w.a.AnonymousAuthenticationFilter      : Set SecurityContextHolder to anonymous ...
TRACE o.s.s.w.a.ExceptionTranslationFilter       : Chain processed normally
TRACE o.s.s.w.a.i.AuthorizationFilter            : Authorized filter invocation ...

日志分析:

  • 请求首先经过 DisableEncodeUrlFilter。
  • 随后 SecurityContextHolderFilter 从 Session 加载上下文(首次访问可能是新建的空上下文)。
  • HeaderWriterFilter 写入安全响应头。
  • CsrfFilter 校验表单中携带的 _csrf Token。
  • UsernamePasswordAuthenticationFilter 提取用户名密码,认证成功后把 Authentication 写入 SecurityContextHolder。
  • 如果认证成功,AnonymousAuthenticationFilter 不会覆盖已认证的身份;如果未认证,它会填充匿名身份。
  • ExceptionTranslationFilter 没有发现异常,放行。
  • AuthorizationFilter 检查当前用户是否有权限访问 /login(登录页通常 permitAll()),校验通过,请求进入 Controller。

结果分析

开启 TRACE 日志后,可以清晰地看到每个过滤器的执行边界。生产环境中建议关闭 TRACE,但在本地调试过滤器顺序或定位"请求为何被 403"时,TRACE 日志是最直接的证据。


示例二:自定义过滤器并精确插入指定位置

场景说明

需要为所有请求添加一个"请求耗时统计"过滤器,要求:

  1. 在 SecurityContext 加载之后执行(因为需要记录当前用户 ID)。
  2. 在 CsrfFilter 之前执行(避免统计逻辑被 CSRF 校验失败打断,导致未记录)。
  3. 不能改变核心过滤器之间的相对顺序。

操作前配置:未指定顺序,自定义过滤器被追加到链尾

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private TimingFilter timingFilter;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .addFilter(timingFilter) // 未指定相对位置,默认追加到链尾
            .authorizeHttpRequests(auth -> auth
                .antMatchers("/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(Customizer.withDefaults());
        return http.build();
    }
}

问题分析:addFilter 不指定相对位置时,Spring Security 会尝试将其放到默认顺序中。如果自定义过滤器的默认顺序与期望不符,它可能出现在 SecurityContextHolderFilter 之前,导致 SecurityContextHolder.getContext().getAuthentication() 返回 null;也可能出现在 AuthorizationFilter 之后,导致未授权请求根本没有机会进入统计逻辑。

操作后配置:使用 addFilterAfter 精确定位

public class TimingFilter extends OncePerRequestFilter {

    private static final Logger logger = LoggerFactory.getLogger(TimingFilter.class);

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                      HttpServletResponse response,
                                      FilterChain filterChain) throws ServletException, IOException {
        long start = System.currentTimeMillis();
        
        // 此时 SecurityContextHolderFilter 已执行,可以获取当前用户
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        String username = (auth != null && auth.isAuthenticated()) ? auth.getName() : "anonymous";

        try {
            filterChain.doFilter(request, response);
        } finally {
            long duration = System.currentTimeMillis() - start;
            logger.info("[Timing] user={}, uri={}, method={}, duration={}ms, status={}",
                username, request.getRequestURI(), request.getMethod(),
                duration, response.getStatus());
        }
    }
}

配置类:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // 在 SecurityContextHolderFilter 之后执行,确保 SecurityContext 已就绪
            // 在 CsrfFilter 之前执行,确保即使 CSRF 校验失败也能记录到请求
            .addFilterAfter(new TimingFilter(), SecurityContextHolderFilter.class)
            .authorizeHttpRequests(auth -> auth
                .antMatchers("/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(Customizer.withDefaults());
        return http.build();
    }
}

结果分析

  • addFilterAfter(new TimingFilter(), SecurityContextHolderFilter.class) 明确将 TimingFilter 插入到 SecurityContextHolderFilter 的下一个位置。
  • 由于 SecurityContextHolderFilter 的顺序是 3,而 CsrfFilter 的顺序是 6,TimingFilter 会位于顺序 4 的位置(Spring Security 自动分配相邻位置),满足"在 SecurityContext 加载之后、在 CsrfFilter 之前"的要求。
  • 在 doFilterInternal 中,SecurityContextHolder.getContext().getAuthentication() 可以稳定拿到当前身份(或匿名身份),不会为 null。

易错场景与面试考点

易错场景:自定义过滤器在 SecurityContextHolderFilter 之前执行,导致 Authentication 为 null

开发者在自定义过滤器中尝试获取当前用户,却得到 null,这是最常见的过滤器顺序错误。

错误代码:

http.addFilterBefore(new MyAuthFilter(), UsernamePasswordAuthenticationFilter.class);

// MyAuthFilter 中:
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
// 结果:auth 为 null,因为 SecurityContextHolderFilter 还没执行!

正确做法:

// 如果自定义过滤器依赖 SecurityContext,必须确保它在 SecurityContextHolderFilter 之后执行
http.addFilterAfter(new MyAuthFilter(), SecurityContextHolderFilter.class);

或者,如果自定义过滤器本身就负责认证(类似 UsernamePasswordAuthenticationFilter),那么它应该将自己产生的 Authentication 手动放入 SecurityContextHolder:

// 认证过滤器内部的标准做法
SecurityContextHolder.getContext().setAuthentication(authenticationResult);

面试考点:面试官常问 "Spring Security 过滤器链中,SecurityContextHolderFilter、UsernamePasswordAuthenticationFilter、AnonymousAuthenticationFilter、ExceptionTranslationFilter、AuthorizationFilter 的先后关系是什么?为什么必须这样排?" 回答要点:

  1. SecurityContextHolderFilter 在最前(第三位),负责从 Session 加载或创建 SecurityContext,确保后续所有过滤器都能通过 SecurityContextHolder.getContext() 访问认证信息。
  2. UsernamePasswordAuthenticationFilter 和 BasicAuthenticationFilter 位于中间(第 8、9 位),负责实际的认证逻辑。认证成功后,它们将 Authentication 写入 SecurityContextHolder。
  3. AnonymousAuthenticationFilter 在认证过滤器之后(第 12 位),如果前面没有任何认证过滤器成功写入身份,它就为请求填充一个匿名身份。这避免了 SecurityContext 为 null 导致后续空指针异常。
  4. ExceptionTranslationFilter 在 AuthorizationFilter 之前(第 13 位),充当异常捕手。如果 AuthorizationFilter(第 14 位)抛出 AccessDeniedException(权限不足),ExceptionTranslationFilter 会将其转译为 403;如果抛出 AuthenticationException(未认证),它会调用 AuthenticationEntryPoint 引导用户登录(如重定向到 /login)。
  5. AuthorizationFilter 在链尾(第 14 位),是最终的访问控制闸口。它根据配置的 URL 权限规则(如 hasRole("ADMIN"))决定是否放行请求到 Controller。
  6. 顺序的本质是依赖关系:后面的过滤器依赖前面过滤器准备好的 SecurityContext;ExceptionTranslationFilter 必须包在 AuthorizationFilter 外面才能捕获其异常。Spring Security 通过 FilterOrderRegistration 硬编码这些顺序,开发者只能用 addFilterBefore / addFilterAfter / addFilterAt 插入自定义过滤器,不能改变核心过滤器之间的相对顺序。

版本说明:本章节示例基于 Spring Security 5.x 语法,使用 SecurityContextHolderFilter、addFilterAfter、antMatchers 以及 @EnableWebSecurity 等 5.x 典型配置风格。在 Spring Security 5.x 的不同版本中,个别过滤器类名可能略有差异(如早期版本使用 SecurityContextPersistenceFilter 而非 SecurityContextHolderFilter),但其职责和顺序位置保持一致。

上一页
SecurityFilterChain