Spring Security 6 :配置生产级 SecurityFilterChain

核心概念: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 安全链通常包含以下四个核心板块:

  1. 基础设施层:关闭 Session、关闭 CSRF、开启跨域、允许 iframe。
  2. 异常处理层:将 401/403 错误转换为标准的 JSON 响应。
  3. 权限控制层:定义哪些 URL 是公开的(静态资源、登录接口、注解标记接口),哪些是受保护的。
  4. 过滤器层:植入自定义的 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,并给浏览器发一个 JSESSIONID Cookie。
  • 现代模式:服务器不保存会话状态。每次请求必须携带 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 默认依赖 SessionBasic Auth 来识别用户。
  • 自定义流程 :我们需要一个过滤器(TokenAuthenticationFilter),负责从 Header 中提取 Token,解析用户 ID,验证有效性,并手动将用户信息存入 SecurityContextHolder
  • 位置 :必须放在 UsernamePasswordAuthenticationFilter 之前。这样,当请求到达后续的鉴权逻辑时,Spring Security 就会认为"该用户已登录"。
相关推荐
Java小白,一起学习2 小时前
AndroidStudio安装教程
java·android-studio
学编程就要猛2 小时前
算法:3.快乐数
java·算法
高山上有一只小老虎2 小时前
如何下载并使用Memory Analyzer (MAT)
java·jvm
华仔啊2 小时前
Java 开发必看:什么时候用 for,什么时候用 Stream?
java·后端
tgethe2 小时前
Java 数组(Array)笔记:从语法到 JVM 内核
java·数据结构
红牛20303 小时前
Nexus Repository搭建maven远程仓库
java·maven·nexus
又是忙碌的一天3 小时前
Maven基本概念
java·maven
@淡 定3 小时前
JVM内存区域划分详解
java·jvm·算法
❀͜͡傀儡师3 小时前
运维问题排查笔记:磁盘、Java进程与SQL执行流程
java·运维·笔记