Spring Security 源码解析(六)无状态 JWT 实践:Session 共享与自定义过滤器

从传统 Session 模式到前后端分离的 JWT 无状态认证,Spring Security 的会话管理策略如何选择?本文深入分析 Session 机制、分布式 Session 共享方案,以及 JWT 自定义过滤器的完整实现。

前言

在上一篇中,我们完整拆解了表单登录的认证流程。但认证成功后,用户的登录状态如何保持?Spring Security 默认使用 Session,但在前后端分离架构下,JWT 才是主流选择。

本文将回答三个核心问题:

  1. Spring Security 的 Session 机制是如何工作的?
  2. 分布式场景下如何实现 Session 共享?
  3. 如何实现 JWT 无状态认证?

一、Spring Security 默认的 Session 模式

1.1 Session 认证流程

sequenceDiagram participant Client as 浏览器 participant Filter as SecurityContextHolderFilter participant AuthFilter as 认证 Filter participant Repo as SecurityContextRepository participant Session as HttpSession Client->>Filter: 第1次请求(无 Session) Filter->>Repo: loadDeferredContext(request) Repo->>Session: 查找 SecurityContext → 无 Repo-->>Filter: 空 SecurityContext Filter->>AuthFilter: chain.doFilter() AuthFilter->>AuthFilter: 执行认证逻辑 AuthFilter->>Repo: saveContext(context) Repo->>Session: 存储 SecurityContext AuthFilter-->>Client: 认证成功响应(Set-Cookie: JSESSIONID=xxx) Client->>Filter: 第2次请求(携带 JSESSIONID) Filter->>Repo: loadDeferredContext(request) Repo->>Session: 查找 SecurityContext → 命中 Repo-->>Filter: 已认证 SecurityContext Filter->>AuthFilter: chain.doFilter() AuthFilter-->>Client: 直接放行(已认证)

要点 :Spring Security 6.x 中,SecurityContextHolderFilter(不要与已废弃的 SecurityContextPersistenceFilter 混淆)只负责加载 SecurityContext,持久化由各认证 Filter(如 AbstractAuthenticationProcessingFilter.successfulAuthentication())显式调用 securityContextRepository.saveContext() 完成。

1.2 关键组件

组件 职责
SecurityContextHolderFilter 在请求开始时从 SecurityContextRepository 延迟加载 SecurityContext,请求结束时清除 SecurityContextHolder(不保存)
DelegatingSecurityContextRepository 默认实现,组合 HttpSessionSecurityContextRepository + RequestAttributeSecurityContextRepository
HttpSessionSecurityContextRepository 将 SecurityContext 存储到 HttpSession 中
SecurityContextHolder 持有当前线程的 SecurityContext(通过 ThreadLocal

历史变更 :Spring Security 5.7 引入了 SecurityContextHolderFilter 替代旧的 SecurityContextPersistenceFilter。核心变化是读写分离 ------SecurityContextHolderFilter 只读(加载),认证 Filter 负责写(保存)。这避免了不必要的持久化开销,尤其是配合 RequestAttributeSecurityContextRepository 可做到请求级别的 SecurityContext 隔离。

1.3 SessionCreationPolicy 四种策略

java 复制代码
http.sessionManagement(session -> 
    session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
策略 说明 适用场景
IF_REQUIRED 仅在需要时创建 Session(默认) 传统 Web 应用
ALWAYS 始终创建 Session 需要保证 Session 存在的场景
NEVER 不主动创建 Session,但如果有则使用 不需要 Session 但不禁止
STATELESS 完全不使用 Session 前后端分离 + JWT

二、分布式 Session 共享:Spring Session Data Redis

2.1 问题场景

在分布式部署中,每个服务器节点都有自己的 Session 存储,导致用户在节点 A 登录后,请求被负载均衡到节点 B 时,Session 不存在,需要重新登录。

2.2 Spring Session Data Redis 方案

原理:将 Session 信息存储到 Redis 中,所有节点共享同一个 Redis,实现 Session 共享。

每个请求无论落到哪个节点,都从同一个 Redis 读取 Session,解决了 Session 不共享的问题。

2.3 实现步骤

Step 1:添加依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

Step 2:启用 Redis Session

java 复制代码
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)  // 30分钟超时
@Configuration
public class SessionConfig {
    // Spring Boot 自动配置 Redis 连接
}

Step 3:配置 Redis 连接

yaml 复制代码
spring:
  data:
    redis:
      host: localhost
      port: 6379
      password: yourpassword

2.4 Spring Session 的核心原理

Spring Session 通过 SessionRepositoryFilter 替换默认的 HttpServletRequest 实现,将所有 getSession() 调用重定向到外部存储(如 Redis):

组件 作用
SessionRepositoryFilter 核心过滤器,替换 HttpServletRequest 和 HttpServletResponse
SessionRepositoryRequestWrapper 包装请求,重写 getSession() 方法
RedisIndexedSessionRepository Session 的 Redis 存储实现
@EnableRedisHttpSession 导入 Spring Session 的自动配置

三、JWT 无状态认证:前后端分离的标准方案

3.1 JWT vs Session 对比

维度 Session JWT
状态 有状态(服务端存储) 无状态(客户端存储)
扩展性 需要Session共享 天然支持分布式
安全性 Cookie 自动携带(CSRF风险) Authorization Header 手动设置
存储位置 服务端内存/Redis 客户端 localStorage
注销 服务端销毁Session 需要额外机制(黑名单/短期Token)
适用场景 传统Web应用 前后端分离/移动端

3.2 JWT 认证架构

sequenceDiagram participant Client as 前端/App participant Filter as JwtAuthenticationFilter participant Provider as JwtTokenProvider participant UDS as UserDetailsService participant SC as SecurityContextHolder Client->>Filter: 请求(Authorization: Bearer xxx) Filter->>Filter: getTokenFromRequest() 提取 Token Filter->>Provider: validateToken(token) Provider-->>Filter: true/false alt Token 有效 Filter->>Provider: getUsernameFromToken(token) Provider-->>Filter: username Filter->>UDS: loadUserByUsername(username) UDS-->>Filter: UserDetails Filter->>Filter: 创建 authenticated Token Filter->>SC: setAuthentication(authentication) SC-->>Filter: SecurityContext 已设置 else Token 无效 Filter->>Filter: 跳过,继续 FilterChain end Filter->>Client: filterChain.doFilter()

无状态的核心 :JWT 过滤器在每次请求时重新解析 Token 并设置 SecurityContext,请求结束后 SecurityContextHolderSecurityContextHolderFilter 清除,不依赖任何服务端存储 。不需要 saveContext()

3.3 完整 JWT 认证实现

Step 1:JWT 工具类

java 复制代码
@Component
public class JwtTokenProvider {
    
    @Value("${jwt.secret}")
    private String secret;
    
    @Value("${jwt.expiration}")
    private long expiration;
    
    // 生成 Token
    public String generateToken(Authentication authentication) {
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + expiration);
        
        return Jwts.builder()
            .setSubject(userDetails.getUsername())
            .setIssuedAt(now)
            .setExpiration(expiryDate)
            .signWith(SignatureAlgorithm.HS512, secret)
            .compact();
    }
    
    // 从 Token 中获取用户名
    public String getUsernameFromToken(String token) {
        Claims claims = Jwts.parser()
            .setSigningKey(secret)
            .parseClaimsJws(token)
            .getBody();
        return claims.getSubject();
    }
    
    // 验证 Token 有效性
    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
            return true;
        } catch (SignatureException | MalformedJwtException ex) {
            // 签名无效
        } catch (ExpiredJwtException ex) {
            // Token 过期
        } catch (UnsupportedJwtException | IllegalArgumentException ex) {
            // Token 格式错误
        }
        return false;
    }
}

Step 2:自定义 JWT 过滤器

java 复制代码
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    @Autowired
    private JwtTokenProvider jwtTokenProvider;
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
            HttpServletResponse response, FilterChain filterChain) 
            throws ServletException, IOException {
        
        // 1. 从 Header 中提取 Token
        String token = getTokenFromRequest(request);
        
        // 2. 验证 Token 并设置认证信息
        if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) {
            
            // 3. 从 Token 中获取用户名
            String username = jwtTokenProvider.getUsernameFromToken(token);
            
            // 4. 加载用户信息
            UserDetails userDetails = 
                userDetailsService.loadUserByUsername(username);
            
            // 5. 创建已认证的 Authentication
            UsernamePasswordAuthenticationToken authentication = 
                UsernamePasswordAuthenticationToken.authenticated(
                    userDetails, null, userDetails.getAuthorities());
            
            authentication.setDetails(
                new WebAuthenticationDetailsSource().buildDetails(request));
            
            // 6. 设置到 SecurityContext
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        
        // 7. 继续过滤器链
        filterChain.doFilter(request, response);
    }
    
    private String getTokenFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

Step 3:SecurityFilterChain 配置

java 复制代码
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .csrf(csrf -> csrf.disable())                    // 关闭 CSRF
        .cors(Customizer.withDefaults())                 // 开启 CORS
        .sessionManagement(session -> 
            session.sessionCreationPolicy(
                SessionCreationPolicy.STATELESS))         // 无状态
        .formLogin(form -> form.disable())               // 关闭表单登录
        .logout(logout -> logout.disable())               // 关闭默认登出
        .addFilterBefore(jwtAuthenticationFilter,         // JWT 过滤器
            UsernamePasswordAuthenticationFilter.class)
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/api/auth/**").permitAll()  // 登录接口放行
            .anyRequest().authenticated()                 // 其他需认证
        );
    
    return http.build();
}

Step 4:登录接口

java 复制代码
@RestController
@RequestMapping("/api/auth")
public class AuthController {
    
    @Autowired
    private AuthenticationManager authenticationManager;
    
    @Autowired
    private JwtTokenProvider jwtTokenProvider;
    
    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
        
        // 1. 认证
        Authentication authentication = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(
                loginRequest.getUsername(), 
                loginRequest.getPassword()
            )
        );
        
        // 2. 生成 Token
        String token = jwtTokenProvider.generateToken(authentication);
        
        // 3. 返回 Token
        return ResponseEntity.ok(new AuthResponse(token));
    }
}

3.4 JWT 过滤器在过滤器链中的位置

当配置 STATELESS 并添加 JWT 过滤器后,Spring Security 6.5.9 实际运行时的核心过滤器链如下(基于 FilterOrderRegistration 源码):

flowchart LR subgraph 核心链[&#34;JWT 模式实际过滤器链(精简版)&#34;] direction LR A[&#34;DisableEncodeUrlFilter&#34;] --> B[&#34;SecurityContextHolderFilter&#34;] B --> C[&#34;HeaderWriterFilter&#34;] C --> D[&#34;CorsFilter&#34;] D --> E[&#34;CsrfFilter(已关闭)&#34;] E --> F[&#34;LogoutFilter&#34;] F --> G[&#34;JwtAuthenticationFilter&#34;] G --> H[&#34;RequestCacheAwareFilter&#34;] H --> I[&#34;AnonymousAuthenticationFilter&#34;] I --> J[&#34;ExceptionTranslationFilter&#34;] J --> K[&#34;AuthorizationFilter&#34;] end style G fill:#c8e6c9,stroke:#2e7d32

顺序解释(按 FilterOrderRegistration 源码级顺序)

Order 过滤器 说明
100 DisableEncodeUrlFilter 禁止 URL 中携带 jsessionid
700 SecurityContextHolderFilter 只加载 SecurityContext,不保存
900 HeaderWriterFilter 添加安全响应头(X-Content-Type-Options 等)
1000 CorsFilter 处理跨域请求
1100 CsrfFilter CSRF 防护(JWT 场景关闭)
1200 LogoutFilter 处理登出请求
~2099 JwtAuthenticationFilter(自定义) 解析 JWT Token,设置 SecurityContext
2100 UsernamePasswordAuthenticationFilter 表单登录(JWT 场景关闭)
3300 RequestCacheAwareFilter 恢复被缓存请求
3700 AnonymousAuthenticationFilter 未认证时填充匿名用户
4000 ExceptionTranslationFilter 异常转换为 HTTP 响应
4200 AuthorizationFilter 执行 URL 级别权限校验

为什么放在 UsernamePasswordAuthenticationFilter 之前?

因为 JWT 场景下不需要表单登录过滤器,自定义 JWT 过滤器替代了它。放在前面可以更早地设置 SecurityContext,避免不必要的后续处理。同时,addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) 会让它插入到 order 2100 之前,在 AnonymousAuthenticationFilter(order 3700)之前执行,这样请求到达 AuthorizationFilter 时已经具备认证信息。


四、会话安全配置

4.1 会话固定攻击防护

java 复制代码
http.sessionManagement(session -> 
    session.sessionFixation().newSession()  // 登录后创建新 Session
);
策略 底层实现 说明
changeSessionId() ChangeSessionIdAuthenticationStrategy 调用 HttpServletRequest.changeSessionId() 更换 Session ID,保留原有 Session 属性(默认,推荐
newSession() SessionFixationProtectionStrategy(关闭属性迁移) 登录后创建新 Session,不保留旧 Session 属性
migrateSession() SessionFixationProtectionStrategy(开启属性迁移) 登录后创建新 Session,并复制旧 Session 属性
none() NullAuthenticatedSessionStrategy 不做任何处理(不推荐,除非有其他防护机制)

源码依据SessionManagementConfigurercreateDefaultSessionFixationProtectionStrategy() 返回 new ChangeSessionIdAuthenticationStrategy(),因此默认策略是 changeSessionId()

在 Spring Security 6.x 中,也可以在 AbstractAuthenticationProcessingFilter.doFilter() 中看到 this.sessionStrategy.onAuthentication(authenticationResult, request, response) 的调用点。

4.2 会话并发控制

java 复制代码
http.sessionManagement(session -> 
    session.maximumSessions(1)                    // 同一用户最多1个Session
           .maxSessionsPreventsLogin(true)        // 阻止新登录(false则踢掉旧Session)
);

4.3 Session 超时配置

yaml 复制代码
server:
  servlet:
    session:
      timeout: 30m    # Session 超时时间
java 复制代码
http.sessionManagement(session -> 
    session.invalidSessionUrl("/login?expired")  // Session 过期后跳转
);

五、完整请求流程对比

5.1 Session 模式(Spring Security 6.x)

6.x 关键变化SecurityContextHolderFilter 只做加载和清除,不负责保存 。保存由各认证 Filter(如 AbstractAuthenticationProcessingFilter.successfulAuthentication())显式调用 securityContextRepository.saveContext() 完成。

5.2 JWT 模式(完全无状态)

JWT 无状态的本质 :每次请求都是全新认证------从 Header 解析 Token → 查库验证 → 设置 Context。请求结束后 SecurityContext 被清除,不调用 saveContext()。服务端不保存任何用户状态。


六、总结

知识点 要点
Session 模式 默认使用 SecurityContextHolderFilter(只加载,不保存),认证 Filter 显式保存
SessionCreationPolicy IF_REQUIRED / ALWAYS / NEVER / STATELESS(源码在 SessionCreationPolicy.java
分布式 Session Spring Session Data Redis,将 Session 存入 Redis 共享
JWT 架构 无状态,Token 存储在客户端,每次请求携带
JWT 过滤器 继承 OncePerRequestFilter,解析 Token → 加载用户 → 设置 SecurityContext
会话固定防护 默认 changeSessionId(),也可选择 newSession() / migrateSession() / none()
会话并发控制 maximumSessions(1).maxSessionsPreventsLogin(true)
方案选择 Session JWT
适用场景 传统Web、SSO 前后端分离、移动端
分布式支持 需要Session共享 天然支持
CSRF风险
注销难度 简单(销毁Session) 较难(需黑名单/短期Token)

下一篇预告 :《Spring Security URL 授权机制源码解析:从 AuthorizeHttpRequestsConfigurer 到 AuthorizationFilter》将深入拆解 http.authorizeHttpRequests() 的配置如何转化为运行时的权限校验逻辑。

相关推荐
乘云数字DATABUFF1 小时前
5分钟部署开源APM Databuff:OpenTelemetry全链路追踪入门实战
运维·后端
荣码1 小时前
LangGraph多Agent协作:3个Agent干活比1个强,但我踩了4个坑
java·python
杨利杰YJlio1 小时前
OpenClaw / clawdbot 是什么?看懂 Agent 体系
前端·后端
SamDeepThinking1 小时前
一条UPDATE语句在MySQL 8.0中到底加了几把锁?
后端·mysql·程序员
CodeSheep1 小时前
他俩只靠写代码,登上了胡润财富榜!
前端·后端·程序员
IT_陈寒1 小时前
React状态更新总是慢半拍?你可能忘了这个默认行为
前端·人工智能·后端
candyTong2 小时前
阿里开源 AI Code Review 工具:ocr review 的执行链路解析
javascript·后端·架构
铁皮饭盒2 小时前
TypeBox 比 Zod.js 校验 快10倍, 还兼容AI 工具调用, 他做对了什么?
前端·javascript·后端
唐青枫3 小时前
Java 虚拟线程实战指南:从 Thread API 到 Spring Boot 高并发应用
java