核心概念:SecurityFilterChain 到底是干嘛的?
在深入代码之前,我们必须先搞清楚 SecurityFilterChain 这个核心概念,以及它和我们平时用的 FilterRegistrationBean 到底有什么区别。很多配置不生效的 Bug,都是因为混淆了这两者。
1. 什么是 SecurityFilterChain?
如果把你的 Spring Boot 应用比作一座 "城堡":
- Tomcat/Servlet 容器 是城堡的外墙。
- Spring Security 是城堡内部一个戒备森严的 "核心金库"。
SecurityFilterChain 就是进入这个"金库"的唯一通道。
它本质上是一个 Bean ,内部包含了一组有序的 安全过滤器链(List<Filter>) 。
当 HTTP 请求经过 Spring Security 的领地时,必须依次通过这条链上的所有关卡(如:CSRF检查 -> 身份认证 -> 权限校验 -> 异常处理),任何一关过不去,请求都会被弹回。
配置它的作用 ,就是为了定制这条通道的规则:
- 我们要拆掉哪些默认的关卡(如 Session)?
- 我们要加装哪些自定义的关卡(如 JWT 校验)?
- 哪些人可以不走通道直接进(放行静态资源)?
2. 关键辨析:SecurityFilterChain vs FilterRegistrationBean
这是初学者最容易晕的地方:"我写了一个 Filter,到底该用哪种方式注册?"
| 特性 | FilterRegistrationBean | SecurityFilterChain (http.addFilter) |
|---|---|---|
| 所属层级 | Servlet 容器层 (Web Layer) 属于 Spring Boot 对原生 Servlet 的封装。 | 安全框架层 (Security Layer) 属于 Spring Security 内部的逻辑。 |
| 管辖范围 | "大门口" 拦截进入应用的所有请求。 | "安检通道内" 只负责处理安全相关的请求。 |
| 典型用途 | 通用基础设施 字符编码、CORS 跨域、Request 日志、TraceId 链路追踪。 | 安全业务逻辑 Token 校验、登录认证、动态权限控制。 |
| 能否感知安全上下文 | 不能 。 它执行时,用户可能还没"登录",它拿不到 SecurityContext。 |
能 。 它是安全链的一部分,负责填充或读取用户的 Authentication 信息。 |
结论:
- 如果你写的是一个 "跟安全无关" 的通用过滤器(比如记录接口耗时),请用
FilterRegistrationBean。 - 如果你写的是一个 "跟身份/权限有关" 的过滤器(比如 JWT 校验),千万不要 用
FilterRegistrationBean把它注册到外层,必须 在SecurityFilterChain中通过http.addFilterBefore()把它安插到安全链的内部。
搞懂了这一点,我们再来看具体的配置代码,一切就顺理成章了。
一、 核心架构图解
一个生产级的 REST API 安全链通常包含以下四个核心板块:
- 基础设施层:关闭 Session、关闭 CSRF、开启跨域、允许 iframe。
- 异常处理层:将 401/403 错误转换为标准的 JSON 响应。
- 权限控制层:定义哪些 URL 是公开的(静态资源、登录接口、注解标记接口),哪些是受保护的。
- 过滤器层:植入自定义的 Token 校验逻辑。
二、 完整配置代码范例
以下代码展示了一个通用的配置模板:
java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Resource
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; // 自定义的 Token 过滤器
@Resource
private RestAuthenticationEntryPoint authenticationEntryPoint; // 自定义的 401 处理
@Resource
private RestAccessDeniedHandler accessDeniedHandler; // 自定义的 403 处理
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// ============================================================
// 1. 基础设施配置 (Infrastructure)
// ============================================================
// 开启跨域 (允许前端调用)
.cors(Customizer.withDefaults())
// 禁用 CSRF (因为使用 Token,不需要 Cookie/Session 防护)
.csrf(AbstractHttpConfigurer::disable)
// 禁用 Session (设置为无状态,服务器不存储用户会话)
.sessionManagement(c -> c.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 允许页面被 iframe 嵌入 (可选,通常用于报表或 H2 控制台)
.headers(c -> c.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
// ============================================================
// 2. 异常处理配置 (Exception Handling)
// ============================================================
// 将 Spring Security 的异常委托给我们自定义的 Handler,返回 JSON 而不是跳转 HTML
.exceptionHandling(c -> c
.authenticationEntryPoint(authenticationEntryPoint) // 处理 401 未登录
.accessDeniedHandler(accessDeniedHandler) // 处理 403 无权限
)
// ============================================================
// 3. 权限路由配置 (Authorization)
// ============================================================
.authorizeHttpRequests(c -> c
// A. 静态资源免鉴权
.requestMatchers(HttpMethod.GET, "/*.html", "/*.css", "/*.js", "/favicon.ico").permitAll()
// B. 公开接口免鉴权 (如登录、注册、Swagger)
.requestMatchers("/auth/login", "/auth/register", "/doc.html").permitAll()
// C. 动态白名单 (核心技巧)
// 这里可以注入一个从注解(@PermitAll)扫描出来的 URL 列表
// .requestMatchers(permitAllUrls).permitAll()
// D. 兜底规则:除了上面声明的,其他所有请求都必须认证
.anyRequest().authenticated()
);
// ============================================================
// 4. 过滤器植入 (Filter Injection)
// ============================================================
// 将自定义的 Token 过滤器加入到过滤器链中
// 建议放在 UsernamePasswordAuthenticationFilter 之前,实现"无感登录"
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
三、 四大核心模块详解
1. 为什么要"去状态化" (Stateless)?
java
.sessionManagement(c -> c.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
- 传统模式 :用户登录后,服务器创建
HttpSession,并给浏览器发一个JSESSIONIDCookie。 - 现代模式:服务器不保存会话状态。每次请求必须携带 Token。
- 配置目的 :这行代码告诉 Spring Security "不要创建 Session,也不要依赖 Session 获取 SecurityContext"。这是 Token 架构的基石。
2. 异常处理为什么要自定义?
java
.exceptionHandling(c -> c.authenticationEntryPoint(...).accessDeniedHandler(...))
Spring Security 默认在遇到认证失败(401)或权限不足(403)时,会尝试跳转到一个默认的 HTML 报错页面。
对于前端(Vue/React)来说,这简直是灾难。前端需要的是 JSON 格式的状态码。
- AuthenticationEntryPoint :负责捕获"未登录"异常,输出
{ code: 401, msg: "请先登录" }。 - AccessDeniedHandler :负责捕获"无权限"异常,输出
{ code: 403, msg: "权限不足" }。
3. 动态权限与白名单设计
java
.authorizeHttpRequests(c -> ...)
这里定义了流量的分流策略。在工程实践中,为了避免每次写新接口都要修改 SecurityConfig 文件,我们通常采用 "动态配置" 的策略:
- 基于注解 :在 Controller 方法上加
@PermitAll,项目启动时扫描这些方法对应的 URL,注入到配置中。 - 基于配置文件 :在
application.yaml中配置security.ignore-urls,在此处读取并放行。
注意顺序 :requestMatchers 的匹配是 "先到先得" 的。必须把 permitAll 的规则写在前面,最后写 anyRequest().authenticated() 作为兜底。
4. 自定义 Token 过滤器 (The Core Filter)
java
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
这是鉴权逻辑生效的关键。
- 默认流程 :Spring Security 默认依赖
Session或Basic Auth来识别用户。 - 自定义流程 :我们需要一个过滤器(
TokenAuthenticationFilter),负责从 Header 中提取 Token,解析用户 ID,验证有效性,并手动将用户信息存入SecurityContextHolder。 - 位置 :必须放在
UsernamePasswordAuthenticationFilter之前。这样,当请求到达后续的鉴权逻辑时,Spring Security 就会认为"该用户已登录"。