过滤器执行顺序
Spring Security 的安全过滤器链内部由一组按固定顺序排列的核心过滤器组成。每个过滤器在特定的时机执行特定的职责:有的负责加载安全上下文,有的负责认证,有的负责授权,有的负责异常转译。理解它们的执行顺序和职责边界,是定位"为什么在自定义过滤器里获取不到当前用户""为什么授权判断先于认证完成"等诡异问题的关键。
定义与作用:顺序即契约
在没有安全框架时,开发者可能会编写多个独立的过滤器:一个做登录校验,一个做权限判断,一个做日志记录。这些过滤器的执行顺序通常由 web.xml 中 <filter-mapping> 的物理顺序决定。这种机制存在严重隐患:
- 顺序不可预测:如果两个过滤器的映射顺序被无意中调换,权限判断可能在登录校验之前执行,导致永远鉴权失败。
- 职责重叠:每个过滤器都自行从 Session 中读取用户状态,没有统一的安全上下文抽象。
- 异常处理不统一:认证失败和权限不足分别由不同的过滤器抛出不同异常,没有统一的转译机制。
Spring Security 将安全过滤器视为一条精密流水线,每个位置都有固定的职责。顺序不是随意的,而是硬编码在 FilterOrderRegistration 中的契约。例如,SecurityContextHolderFilter 必须在 UsernamePasswordAuthenticationFilter 之前,因为后者依赖前者加载或初始化 SecurityContext;ExceptionTranslationFilter 必须在 AuthorizationFilter 之前,因为它需要捕获后者抛出的 AccessDeniedException。
核心原理:固定顺序与职责分工
Spring Security 5.x 中核心安全过滤器的标准执行顺序如下:
| 顺序 | 过滤器 | 核心职责 |
|---|---|---|
| 1 | DisableEncodeUrlFilter | 禁止响应 URL 中携带 jsessionid,防止 Session ID 泄露 |
| 2 | WebAsyncManagerIntegrationFilter | 将 SecurityContext 绑定到异步请求(WebAsyncManager) |
| 3 | SecurityContextHolderFilter / SecurityContextPersistenceFilter | 从 Session 或请求中加载 SecurityContext,绑定到当前线程 |
| 4 | HeaderWriterFilter | 写入安全响应头(X-Frame-Options、X-Content-Type-Options 等) |
| 5 | CorsFilter | 处理跨域预检请求(OPTIONS)和实际跨域响应头 |
| 6 | CsrfFilter | 生成和校验 CSRF Token,防御跨站请求伪造 |
| 7 | LogoutFilter | 处理 /logout 请求,销毁 Session、清除 Cookie、调用登出处理器 |
| 8 | UsernamePasswordAuthenticationFilter | 拦截 POST /login,执行表单登录认证 |
| 9 | BasicAuthenticationFilter | 解析 Authorization: Basic ... 请求头,执行 HTTP Basic 认证 |
| 10 | RequestCacheAwareFilter | 恢复被登录中断前的原始请求(如访问受保护页面被重定向到登录页后) |
| 11 | SecurityContextHolderAwareRequestFilter | 包装 HttpServletRequest,使其支持 HttpServletRequest.getUserPrincipal() 等 Servlet 3.0 安全方法 |
| 12 | AnonymousAuthenticationFilter | 如果当前请求仍未认证,为其填充一个匿名 Authentication(anonymousUser,权限 ROLE_ANONYMOUS) |
| 13 | ExceptionTranslationFilter | 捕获下游过滤器抛出的 AuthenticationException(转译为 401/重定向登录)和 AccessDeniedException(转译为 403) |
| 14 | AuthorizationFilter / 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校验表单中携带的_csrfToken。UsernamePasswordAuthenticationFilter提取用户名密码,认证成功后把Authentication写入SecurityContextHolder。- 如果认证成功,
AnonymousAuthenticationFilter不会覆盖已认证的身份;如果未认证,它会填充匿名身份。 ExceptionTranslationFilter没有发现异常,放行。AuthorizationFilter检查当前用户是否有权限访问/login(登录页通常permitAll()),校验通过,请求进入 Controller。
结果分析
开启 TRACE 日志后,可以清晰地看到每个过滤器的执行边界。生产环境中建议关闭 TRACE,但在本地调试过滤器顺序或定位"请求为何被 403"时,TRACE 日志是最直接的证据。
示例二:自定义过滤器并精确插入指定位置
场景说明
需要为所有请求添加一个"请求耗时统计"过滤器,要求:
- 在
SecurityContext加载之后执行(因为需要记录当前用户 ID)。 - 在
CsrfFilter之前执行(避免统计逻辑被 CSRF 校验失败打断,导致未记录)。 - 不能改变核心过滤器之间的相对顺序。
操作前配置:未指定顺序,自定义过滤器被追加到链尾
@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的先后关系是什么?为什么必须这样排?" 回答要点:
SecurityContextHolderFilter在最前(第三位),负责从 Session 加载或创建SecurityContext,确保后续所有过滤器都能通过SecurityContextHolder.getContext()访问认证信息。UsernamePasswordAuthenticationFilter和BasicAuthenticationFilter位于中间(第 8、9 位),负责实际的认证逻辑。认证成功后,它们将Authentication写入SecurityContextHolder。AnonymousAuthenticationFilter在认证过滤器之后(第 12 位),如果前面没有任何认证过滤器成功写入身份,它就为请求填充一个匿名身份。这避免了SecurityContext为null导致后续空指针异常。ExceptionTranslationFilter在AuthorizationFilter之前(第 13 位),充当异常捕手。如果AuthorizationFilter(第 14 位)抛出AccessDeniedException(权限不足),ExceptionTranslationFilter会将其转译为 403;如果抛出AuthenticationException(未认证),它会调用AuthenticationEntryPoint引导用户登录(如重定向到/login)。AuthorizationFilter在链尾(第 14 位),是最终的访问控制闸口。它根据配置的 URL 权限规则(如hasRole("ADMIN"))决定是否放行请求到 Controller。- 顺序的本质是依赖关系:后面的过滤器依赖前面过滤器准备好的
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),但其职责和顺序位置保持一致。