SecurityFilterChain
SecurityFilterChain 是 Spring Security 的核心抽象,代表一条作用于 HTTP 请求的安全过滤器链。每个进入应用的请求都会依次经过链中的若干 Filter,完成认证(Authentication)、授权(Authorization)、CSRF 防护、会话管理等工作。
底层机制:从 Servlet Filter 到 Spring Security
Servlet 规范本身有 Filter 接口和 FilterChain 接口。Spring Security 通过以下两层代理接入这套机制:
DelegatingFilterProxy:在 Servlet 容器(Tomcat)层面注册的Filter,作用是把请求桥接到 Spring 容器中的 Bean。它本身不做安全逻辑,只是一个"入口代理"。FilterChainProxy:Spring 容器中的真正实现,也是一个特殊的Filter。它持有所有注册的SecurityFilterChainBean,收到请求后,按顺序匹配第一条能处理该 URL 的链,然后依次执行链中的每个Filter。
HTTP 请求
→ Tomcat FilterChain
→ DelegatingFilterProxy(Servlet Filter,桥接到 Spring,Bean 名固定为 springSecurityFilterChain)
→ FilterChainProxy(Spring Bean,收集所有 SecurityFilterChain)
→ SecurityFilterChain 1(匹配 /api/**)
UsernamePasswordAuthenticationFilter
BearerTokenAuthenticationFilter
ExceptionTranslationFilter
AuthorizationFilter
...
→ SecurityFilterChain 2(匹配 /**)
...
← 链中最后一个 Filter 放行(chain.doFilter()),控制权交回 DelegatingFilterProxy
→ Tomcat FilterChain 继续执行其余 Filter(如果有)
→ DispatcherServlet
→ HandlerMapping 找到目标 Controller 方法
→ Controller
SecurityFilterChain Bean 如何被收集并接入请求
在 @Configuration 类里写的 @Bean public SecurityFilterChain filterChain(...) 方法,只是往 IoC 容器里注册了一个数据对象——具体实现类是 DefaultSecurityFilterChain,内部就是一个 Filter 列表加一个 RequestMatcher。真正让这份数据接入请求处理流程,还需要另外两步:
- Spring Boot 的自动配置(
SecurityFilterAutoConfiguration)会创建一个FilterChainProxyBean,构造时把容器里所有SecurityFilterChain类型的 Bean 都收集起来(可以有多个,用@Order排优先级),并以固定 Bean 名springSecurityFilterChain注册。 DelegatingFilterProxy是在 Servlet 容器层面注册的真正Filter,它按名字springSecurityFilterChain到 Spring 容器里查找对应的 Bean(也就是上一步的FilterChainProxy),把每个进来的请求都转发给它处理。
所以一次请求要先经过 Tomcat 的 Filter 链,被 DelegatingFilterProxy 接住转发给 FilterChainProxy;FilterChainProxy 用 matches() 找到第一条匹配的 SecurityFilterChain,取出这条链的 Filter 列表逐个执行。认证、授权都通过后,链上最后一个 Filter 调用 chain.doFilter() 放行,请求才会继续往下走,最终到达 DispatcherServlet 并路由到 Controller 方法;一旦某个 Filter 判定失败(比如未认证),会直接短路返回响应(401/403),请求根本不会到达 Controller。
定义一个 SecurityFilterChain
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain apiSecurityChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**") // 只匹配 /api/** 路径
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态,适合 JWT
)
.csrf(csrf -> csrf.disable()); // REST API 通常关闭 CSRF
return http.build();
}
}
HttpSecurity 是 SecurityFilterChain 的构建器,调用 .build() 后生成链实例并注册为 Bean。
多条链:按 URL 区分安全策略
可以注册多个 SecurityFilterChain Bean,用 @Order 控制优先级(数字越小越优先)。FilterChainProxy 会用第一条匹配的链处理请求,后续链不再参与。
@Bean
@Order(1) // 优先级更高
public SecurityFilterChain apiChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain webChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login", "/css/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
return http.build();
}
多条 SecurityFilterChain 之间是互斥的,不是首尾相连、逐条执行:一次请求按 URL 分流,只会命中第一条 securityMatcher 匹配的链,其余链完全不参与这次请求。请求一旦流到某一条被选中的链上,才会依次流过这条链里的每一个 Filter——只要没有 Filter 中途拦截(比如未认证被短路返回 401),就会在链尾放行,继续走到 DispatcherServlet 和 Controller。
链中的关键内置 Filter
| Filter | 作用 |
|---|---|
SecurityContextHolderFilter |
加载/保存 SecurityContext(当前用户信息) |
UsernamePasswordAuthenticationFilter |
处理表单登录(POST /login) |
BearerTokenAuthenticationFilter |
处理 JWT/OAuth2 Bearer Token |
ExceptionTranslationFilter |
将认证/授权异常转换为 HTTP 401/403 响应 |
AuthorizationFilter |
最终的权限校验(替代旧的 FilterSecurityInterceptor) |
CsrfFilter |
CSRF Token 校验 |
CorsFilter |
处理跨域预检请求 |
与 GoF 设计模式的关系
SecurityFilterChain(以及底层 Servlet Filter/FilterChain 机制)最贴近 GoF 的 责任链模式(Chain of Responsibility):
public interface Filter {
void doFilter(ServletRequest request, ServletResponse response, FilterChain chain);
}
- 每个
Filter拿到的FilterChain就是"指向下一个处理者的引用"。 chain.doFilter(request, response)就是"转交给下一个处理者"的动作。- 调用者(
FilterChainProxy)不需要知道链里具体有多少个、哪些Filter,只管触发第一个,后续完全由链自己传递——这正是责任链模式"发起者与处理者解耦"的意图。
不完全严格的地方在于:GoF 经典责任链通常是"某一个处理者处理后就终止"(比如异常处理链,谁能处理谁就截胡),而 Servlet Filter 更常见的用法是环绕式——doFilter 在调用 chain.doFilter() 之前和之后都能插入逻辑(比如 ExceptionTranslationFilter 在调用链后半段捕获异常),这种"前置 + 调用下一个 + 后置"的写法带了一点装饰器模式(Decorator)的味道。但从"谁触发下一个"这个结构本质上看,还是责任链,装饰器更多是从"层层包装、增强职责"的角度补充解释,不影响结论。
permitAll、authenticated、hasRole
authorizeHttpRequests 中的规则从上到下第一条匹配即生效,顺序很重要:
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll() // 公开访问
.requestMatchers("/admin/**").hasRole("ADMIN") // 需要 ADMIN 角色
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated() // 其余需要登录
);
hasRole("ADMIN") 等价于 hasAuthority("ROLE_ADMIN")——Spring Security 约定角色名在存储时带 ROLE_ 前缀,hasRole 会自动补充。
认证流程:Filter、AuthenticationManager、UserDetailsService 的职责划分
一次完整的表单登录认证,会依次经过三层职责不同的组件:Filter 负责触发和收尾、AuthenticationManager 负责调度、UserDetailsService 负责加载用户数据。下面用 comments-tree 项目里真实的 SecurityConfig 来说明每一层具体做了什么。
整体链路
POST /session (nameOrEmail + password)
-> UsernamePasswordAuthenticationFilter (generated from formLogin config)
-> AuthenticationManager.authenticate(token)
-> ProviderManager delegates to DaoAuthenticationProvider
-> UserDetailsService.loadUserByUsername(nameOrEmail)
-> PasswordEncoder.matches(rawPassword, encodedPassword)
<- returns authenticated Authentication
-> written into SecurityContext
-> AuthenticationSuccessHandler / AuthenticationFailureHandler
Filter:入口与出口
comments-tree 用 formLogin 配置表单登录,Spring Security 在背后生成 UsernamePasswordAuthenticationFilter,拦截 /session 上的 POST 请求:
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.POST, "/session").permitAll()
.requestMatchers(HttpMethod.GET, "/session").permitAll()
.anyRequest().authenticated()
)
.exceptionHandling(ex -> ex
.accessDeniedHandler(restfulAccessDeniedHandler())
.authenticationEntryPoint(restAuthenticationEntryPoint())
)
.formLogin(form -> form
.loginProcessingUrl("/session")
.usernameParameter("nameOrEmail")
.successHandler(loginAuthenticationSuccessHandler())
.failureHandler(formLoginFailedHandler())
);
return httpSecurity.build();
}
Filter 层的职责有两块:
- 触发认证:从请求里取出
nameOrEmail和password,封装成未认证的UsernamePasswordAuthenticationToken,转交给AuthenticationManager。 - 收尾与异常翻译:认证结果出来后,Filter 决定调用
AuthenticationSuccessHandler还是AuthenticationFailureHandler;未登录访问受保护资源时,由ExceptionTranslationFilter调用AuthenticationEntryPoint(对应 comments-tree 的RestAuthenticationEntryPoint,返回 401);已登录但权限不足时调用AccessDeniedHandler(RestfulAccessDeniedHandler,返回 403)。
Filter 本身不做用户查找和密码比对,这部分工作全部委托出去。
AuthenticationManager:只做调度,不做校验
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
comments-tree 没有自定义 AuthenticationProvider,直接用 AuthenticationConfiguration 拿到 Spring Boot 组装好的 ProviderManager。ProviderManager 内部维护一组 AuthenticationProvider,收到 UsernamePasswordAuthenticationToken 后,找到能处理这个 Token 类型的 DaoAuthenticationProvider,把认证工作转交给它。
AuthenticationManager 的职责因此很单一:接收未认证的 Authentication,选择合适的 Provider,返回认证结果(成功则是已认证的 Authentication,失败抛 AuthenticationException)。它自己不知道密码是怎么存的,也不知道用户数据从哪来。
UserDetailsService:只管加载数据,不管比对密码
@Service
public class UserService implements UserDetailsService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String nameOrEmail) throws UsernameNotFoundException {
User user = getUserByNameOrEmail(nameOrEmail);
if (user == null) {
throw new UsernameNotFoundException("User not found: " + nameOrEmail);
}
return new CommentsTreeUserDetails(user);
}
public User getUserByNameOrEmail(String nameOrEmail) {
if (Utils.isEmail(nameOrEmail)) {
return getUserByEmail(nameOrEmail);
}
return getUserByName(nameOrEmail);
}
}
UserService 同时承担业务层(注册、查询用户)和 UserDetailsService 两个角色。loadUserByUsername 支持用户名或邮箱两种方式登录,查不到人直接抛 UsernameNotFoundException,这是 UserDetailsService 唯一的契约方法。
密码比对不在这里发生。DaoAuthenticationProvider 拿到 loadUserByUsername 返回的 UserDetails(内部含 BCrypt 加密后的密码)后,自己调用 PasswordEncoder.matches(rawPassword, userDetails.getPassword()) 做比对,一致才算认证通过。
PasswordEncoder 为什么单独抽出一个 Configuration
@Configuration
public class PasswordEncoderConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
UserService 的构造函数需要注入 PasswordEncoder(用于注册时哈希密码),而 PasswordEncoder 这个 Bean 原本是在 SecurityConfig 里定义的。如果放在一起,会形成 SecurityConfig -> UserService(构造参数)-> PasswordEncoder(SecurityConfig 里定义) 的循环依赖。把 PasswordEncoder 单独抽成一个 @Configuration 类,让两边都依赖这个更小的配置类,就消掉了这个环。
三者职责小结
| 组件 | 职责 | 不做的事 |
|---|---|---|
Filter(UsernamePasswordAuthenticationFilter) |
从请求提取凭证、触发认证、处理认证结果(成功/失败/异常翻译) | 不查用户、不比对密码 |
AuthenticationManager(ProviderManager) |
调度合适的 AuthenticationProvider,返回认证结果 |
不知道用户数据来源、不知道密码怎么存 |
UserDetailsService(UserService) |
根据用户名/邮箱加载 UserDetails |
不做密码比对 |
PasswordEncoder(在 DaoAuthenticationProvider 内部使用) |
比对明文密码与加密密码 | 不参与用户查找 |
这套划分本质上是单一职责原则的体现:账号来源变化(比如接入 LDAP)只需要换 UserDetailsService 的实现,认证方式变化(比如接入 OAuth2)只需要加新的 AuthenticationProvider,Filter 和调度逻辑都不用动。
Remember Me
comments-tree 还开启了 rememberMe,用持久化令牌(PersistentTokenRepository)实现跨设备的登录状态失效控制,详见独立文章 Spring Security Remember Me 实现机制。
维护记录
| 时间 | 修改内容 | 原因 |
|---|---|---|
| 2026-07-01 | 新增「认证流程:Filter、AuthenticationManager、UserDetailsService 的职责划分」章节,结合 comments-tree 项目真实的 SecurityConfig/UserService/PasswordEncoderConfig 代码说明各组件职责 | 面试准备时整理认证流程知识点,补充真实项目案例 |
| 2026-07-01 | 删除「authorizeRequests(旧 API)」「spring security」(含语法错误的 pom.xml、.authorizeRequests() 旧写法、过时 PlantUML 类图、掘金/简书转载内容)以及「spring security 拦截器」(含已被 6.x 替换的 SecurityContextPersistenceFilter、FilterSecurityInterceptor 等旧 Filter 列表)三节 |
内容过时、与前文已更新的 6.x 说明矛盾,且转载内容与 remix 标签定性不符 |
| 2026-07-01 | 在「链中的关键内置 Filter」后新增「与 GoF 设计模式的关系」小节,说明 SecurityFilterChain 对应责任链模式,并站内链接到 责任链模式 |
补充设计模式视角,关联站内已有的责任链模式文章 |
| 2026-07-01 | 补充「SecurityFilterChain Bean 如何被收集并接入请求」小节,说明 @Bean 注册的 SecurityFilterChain 如何被 FilterChainProxy 收集、DelegatingFilterProxy 按 Bean 名桥接,并更新调用链图补充到 DispatcherServlet/Controller 的走向 |
说明请求先经过 SecurityFilterChain 才到达 Controller 的完整机制 |
| 2026-07-01 | 「底层机制」段落站内链接到 Servlet Filter;「多条链」一节补充说明多个 SecurityFilterChain 互斥(按 URL 分流,只命中一条),命中的链上会依次流过每一个 Filter | 澄清多条 SecurityFilterChain 之间是互斥关系而非串联,避免与责任链模式内部 Filter 顺序执行混淆 |
| 2026-07-01 | 新增「Remember Me 实现机制」章节,对比简单哈希令牌与 PersistentTokenRepository 两种实现,结合 comments-tree 的 JdbcTokenRepositoryImpl 配置说明跨设备失效控制的原理 |
面试准备时整理 remember-me 机制与跨设备失效控制概念 |
| 2026-07-01 | 「Remember Me」章节内容拆分为独立文章 Spring Security Remember Me 实现机制,本文只保留一行引用 | 内容改写为独立文档,避免两篇文章重复维护同一份内容 |