乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Spring Security 基础

    • 本章定位
    • Spring Security 是什么
    • DelegatingFilterProxy
    • 安全过滤器链
    • SecurityFilterChain
    • 过滤器执行顺序
  • 第2章 认证

    • 本章定位
    • Authentication
    • 认证流程
    • AuthenticationManager
    • ProviderManager
    • DaoAuthenticationProvider
    • UserDetails
    • UserDetailsService
    • PasswordEncoder
    • BCryptPasswordEncoder
    • DelegatingPasswordEncoder
    • 表单登录
    • SecurityContext
    • SecurityContextHolder
    • UsernamePasswordAuthenticationToken
  • 第3章 授权

    • 本章定位
    • 授权模型
    • GrantedAuthority
    • AccessDecisionManager
    • AccessDecisionVoter
    • URL 级别授权
    • 方法级别安全
    • @PreAuthorize
    • @PostAuthorize
    • @PreFilter
    • @PostFilter
    • @Secured
    • RoleHierarchy
  • 第4章 过滤器链

    • 本章定位
    • FilterChainProxy
    • SecurityContextHolderFilter
    • LogoutFilter
    • BasicAuthenticationFilter
    • CsrfFilter
    • CorsFilter
    • HeaderWriterFilter
    • AnonymousAuthenticationFilter
    • RequestCacheAwareFilter
    • ExceptionTranslationFilter
    • FilterSecurityInterceptor
  • 第5章 会话管理

    • 本章定位
    • 会话管理
    • SessionFixation
    • 会话并发控制
    • SessionCreationPolicy
    • RememberMe
  • 第6章 JWT

    • 本章定位
    • JWT
    • JwtDecoder
    • JWT 认证
    • JwtAuthenticationConverter
  • 第7章 OAuth2

    • 本章定位
    • OAuth2 基础
    • OAuth2 Client
    • OAuth2 Resource Server
    • 第三方登录配置
  • 第8章 攻击防护

    • 本章定位
    • CSRF 跨站请求伪造防护
    • CORS 跨域防护
    • Clickjacking 点击劫持防护
    • 安全响应头
    • Session Fixation 会话固定防护
  • 第9章 测试

    • 本章定位
    • 安全测试
    • @WithMockUser
    • 最佳实践

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

维度STATELESSNEVER
创建新 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,导致用户看起来"已经登录"。

真正的无状态必须满足两个条件:

  1. 不创建 Session:使用 STATELESS 而非 NEVER
  2. 不读取 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) 风格)。

上一页
会话并发控制
下一页
RememberMe