SessionCreationPolicy
定义与作用
SessionCreationPolicy 是 Spring Security 定义会话创建策略的枚举,决定安全过滤器链在何种场景下创建或访问 HttpSession。没有安全框架时,开发者需要在每个 Servlet 入口手动判断 request.getSession() 与 request.getSession(false) 的区别:前者在 Session 不存在时强制创建,后者仅复用已存在。稍有不慎,无状态 API 接口也会无端创建会话,导致内存浪费、集群复制开销和横向扩展困难。
Spring Security 通过 SessionCreationPolicy 在框架层面统一管控会话创建行为,四种策略覆盖从纯无状态到强制有状态的完整光谱:
| 策略 | 含义 | 典型场景 |
|---|---|---|
STATELESS | 绝不创建 Session,也不使用已存在的 | JWT 无状态 API、REST Resource Server |
NEVER | 不主动创建,但可以使用已存在的 | 混合架构(OAuth2 认证后转发至有状态前端) |
IF_REQUIRED | 按需创建(默认) | 传统表单登录 Web 应用 |
ALWAYS | 始终创建 Session | 某些强制需要 Session 的集成场景(如 JSP 标签库) |
核心原理
SessionCreationPolicy 通过 SessionManagementConfigurer 配置,最终影响 SecurityContextRepository 的选取:
STATELESS→ 使用NullSecurityContextRepository:不读取、不写入SecurityContext到 Session,完全依赖每次请求携带的凭据(如 JWT Token)NEVER/IF_REQUIRED/ALWAYS→ 使用HttpSessionSecurityContextRepository:从 Session 读取/写入SecurityContext,仅在IF_REQUIRED和ALWAYS时允许创建新 Session
SessionManagementFilter 通过 SessionCreationPolicy 决定是否允许调用 request.getSession():
// 策略转换逻辑(简化)
switch (sessionCreationPolicy) {
case STATELESS:
request.getSession(false); // 不创建,且忽略已存在的
break;
case NEVER:
request.getSession(false); // 不创建,但可用已存在的
break;
case IF_REQUIRED:
request.getSession(); // 按需创建(如认证成功后需要存储 SecurityContext)
break;
case ALWAYS:
request.getSession(true); // 强制创建
break;
}
关键区分点:STATELESS vs NEVER
| 维度 | STATELESS | NEVER |
|---|---|---|
| 创建新 Session | ❌ 禁止 | ❌ 不主动创建 |
| 使用已存在 Session | ❌ 忽略 | ✅ 复用 |
| 安全上下文来源 | 每次请求重新认证(如 JWT) | 可能从已有 Session 恢复 |
| 典型副作用 | 匿名用户每次请求都重新创建 AnonymousAuthenticationToken | 已有 Session 中的 SecurityContext 可被恢复 |
常见误区:以为
NEVER等同于"无状态"。实际上NEVER只是"不主动创建新 Session",如果容器或上游 Filter 已经创建了 Session,Spring Security 仍会从其中读取SecurityContext,导致认证状态被保留。
四种策略的决策流程
示例一:JWT 无状态 API 使用 STATELESS
场景说明
前后端分离的 REST API 服务使用 JWT 认证,每个请求在 Authorization Header 中携带 Token。服务端不应创建任何 HttpSession,否则产生无意义的内存开销和集群同步问题。
操作前配置
使用默认策略 IF_REQUIRED,API 调用触发认证后,Spring Security 自动创建 Session 存储 SecurityContext:
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ApiSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.antMatchers("/api/orders/**").hasRole("USER")
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
// 未配置 sessionCreationPolicy,默认 IF_REQUIRED
// 结果:JWT 认证成功后,Spring Security 创建了 Session 存储 SecurityContext
}
}
问题分析:即使使用 JWT,认证成功后 SecurityContextPersistenceFilter 会将 SecurityContext 写入 HttpSession,导致:
- 服务端内存中积累大量 Session
- 负载均衡下 Session 复制开销
- 集群节点间需要 Session 同步,违背无状态设计初衷
操作后配置
显式声明 STATELESS,彻底禁用 Session 的读写:
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ApiSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态
.and()
.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.antMatchers("/api/orders/**").hasRole("USER")
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
}
结果分析
SecurityContextPersistenceFilter被替换为使用NullSecurityContextRepository- 每次请求
SecurityContextHolder中都是空的,JWT Filter 从 Header 解析 Token 后重新构建Authentication放入SecurityContextHolder - 响应结束后
SecurityContext不被写入 Session,内存中无会话残留 - 服务端水平扩展时无需考虑 Session 复制,实现真正的无状态
示例二:混合架构使用 NEVER 复用上游 Session
场景说明
企业网关(如 Apereo CAS)已完成单点登录认证,并将认证信息写入 HttpSession。下游 Spring Security 应用不应创建新 Session,但需要读取网关写入的 Session 属性以识别用户身份。
操作前配置
使用 IF_REQUIRED,Spring Security 在发现已有 Session 后仍可能创建新的 SecurityContext 存储,覆盖网关写入的用户信息:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
.authorizeRequests()
.anyRequest().authenticated();
// 问题:IF_REQUIRED 允许创建新 Session,可能覆盖网关 Session 中的属性
}
操作后配置
使用 NEVER,确保 Spring Security 只读取已存在的 Session,绝不创建新的:
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SsoSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.NEVER) // 不创建,只复用
.and()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.addFilterAfter(ssoAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Bean
public SsoAuthenticationFilter ssoAuthenticationFilter() {
return new SsoAuthenticationFilter();
}
}
// 自定义 Filter 从已有 Session 读取网关写入的用户信息
public class SsoAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
HttpSession session = request.getSession(false); // 不创建新 Session
if (session != null) {
String username = (String) session.getAttribute("CAS_USER");
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(username, null,
Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")));
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
filterChain.doFilter(request, response);
}
}
结果分析
NEVER策略下,HttpSessionSecurityContextRepository仅调用request.getSession(false)- 如果 CAS 网关已创建 Session,Spring Security 从其中读取并可能写入
SecurityContext;但如果没有 Session,则不会触发创建 - 自定义
SsoAuthenticationFilter从已有 Session 的CAS_USER属性提取用户名,构建Authentication放入SecurityContextHolder - 若网关 Session 未建立,请求无认证信息,后续授权过滤器返回 403 或跳转登录
易错场景:NEVER 不等于无状态
问题描述
开发者想将 API 改为无状态,但配置 NEVER 后,通过浏览器访问时仍能从 Session 恢复登录状态,关闭 JWT Filter 后请求仍能访问受保护资源。
根本原因
NEVER 策略仅阻止 Spring Security 创建新 Session,并不阻止应用其他组件(如 Spring MVC、Tomcat、自定义 Filter)创建 Session。一旦 Session 存在,HttpSessionSecurityContextRepository 仍会从该 Session 读取 SecurityContext,导致用户看起来"已经登录"。
真正的无状态必须满足两个条件:
- 不创建 Session:使用
STATELESS而非NEVER - 不读取 Session:
NullSecurityContextRepository完全忽略HttpSession
错误配置
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.NEVER); // 不是无状态!
正确配置
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 真正的无状态
面试考点
问:
NEVER和STATELESS有什么区别?什么场景下选择NEVER?答:
NEVER表示 Spring Security 不主动创建新 Session,但如果 Session 已经存在(由容器或其他组件创建),Spring Security 仍会从该 Session 读取SecurityContext。STATELESS则彻底禁用 Session 的读写,使用NullSecurityContextRepository,每次请求都重新认证。选择
NEVER的典型场景是混合架构:上游组件(如 SSO 网关、Filter)已经创建了 Session 并写入用户信息,Spring Security 需要复用该 Session 但不应自己创建新的。选择STATELESS的典型场景是纯无状态 API(如 JWT、HTTP Basic),服务端不维护任何会话状态。
版本说明:本章节基于 Spring Security 5.x(
WebSecurityConfigurerAdapter+antMatchers+@EnableGlobalMethodSecurity(prePostEnabled=true)风格)。