从传统 Session 模式到前后端分离的 JWT 无状态认证,Spring Security 的会话管理策略如何选择?本文深入分析 Session 机制、分布式 Session 共享方案,以及 JWT 自定义过滤器的完整实现。
前言
在上一篇中,我们完整拆解了表单登录的认证流程。但认证成功后,用户的登录状态如何保持?Spring Security 默认使用 Session,但在前后端分离架构下,JWT 才是主流选择。
本文将回答三个核心问题:
- Spring Security 的 Session 机制是如何工作的?
- 分布式场景下如何实现 Session 共享?
- 如何实现 JWT 无状态认证?
一、Spring Security 默认的 Session 模式
1.1 Session 认证流程
要点 :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 认证架构
无状态的核心 :JWT 过滤器在每次请求时重新解析 Token 并设置 SecurityContext,请求结束后
SecurityContextHolder被SecurityContextHolderFilter清除,不依赖任何服务端存储 。不需要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 源码):
顺序解释(按 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 |
不做任何处理(不推荐,除非有其他防护机制) |
源码依据 :
SessionManagementConfigurer中createDefaultSessionFixationProtectionStrategy()返回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()的配置如何转化为运行时的权限校验逻辑。