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

    • 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
    • 最佳实践

Session_Fixation

会话固定攻击(Session Fixation Attack)是指攻击者预先获取一个有效的 Session ID,诱骗目标用户使用该 Session ID 登录,登录后攻击者即可使用同一 Session ID 冒充用户身份。Spring Security 默认在用户认证成功后更换 Session ID,通过 SessionFixationProtectionStrategy 阻断此类攻击。


定义与作用

一句话定义:攻击者让用户使用攻击者已知的 Session ID 登录,登录成功后该 Session ID 绑定到用户身份,攻击者直接凭此 Session ID 访问用户会话。

阶段攻击者行为用户状态Session ID 归属
准备访问目标站点,获取服务器分配的 Session ID:abc123未登录匿名会话,属攻击者
诱骗发送含 JSESSIONID=abc123 的链接给用户未登录匿名会话,用户被迫使用
登录无操作用户登录成功认证会话,绑定用户身份,ID 仍为 abc123
窃取用 JSESSIONID=abc123 访问站点用户已登录攻击者以用户身份通过认证

Spring Security 的 SessionFixationProtectionStrategy 在 UsernamePasswordAuthenticationFilter 认证成功后被触发,默认采用 migrateSession() 策略:创建新的 Session ID,将旧 Session 的所有属性迁移到新 Session,旧 Session 失效。攻击者持有的旧 Session ID 不再有效。


攻击原理与防护流程

核心原理:认证成功点是更换 Session ID 的关键时机。Spring Security 在 AbstractAuthenticationProcessingFilter.successfulAuthentication() 中调用 SessionAuthenticationStrategy 接口。SessionFixationProtectionStrategy 是其默认实现,通过 HttpServletRequest.changeSessionId() 或 Session.invalidate() + request.getSession(true) 实现 ID 变更,同时保留原 Session 的属性(如购物车、表单数据等),对用户无感知。


核心配置:三种防护策略

Spring Security 5.x 提供三种 Session Fixation 防护策略,通过 sessionManagement().sessionFixation() 配置:

策略方法行为旧 Session 属性
迁移(默认)migrateSession()更换 Session ID,保留属性全部迁移到新 Session
新建newSession()更换 Session ID,不保留属性全部丢弃
不防护none()不更换 Session ID原样保留

1. 默认 migrateSession(推荐)

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/login", "/public/**").permitAll()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            .and()
            .formLogin()
                .loginPage("/login")
                .permitAll()
            .and()
            .sessionManagement()
                .sessionFixation().migrateSession(); // 默认行为,显式声明
    }
}

2. 新建 Session(高安全场景,丢弃所有匿名属性)

@EnableWebSecurity
public class HighSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/login").permitAll()
                .anyRequest().authenticated()
            .and()
            .formLogin()
                .permitAll()
            .and()
            .sessionManagement()
                .sessionFixation().newSession(); // 登录后完全清空匿名 Session 属性
    }
}

适用场景:金融、支付等高安全场景。用户登录前可能有匿名购物车或临时表单数据,出于安全考虑,登录后完全丢弃这些数据,避免攻击者提前在匿名 Session 中植入恶意数据,登录后随 Session 迁移进入认证会话。

3. none()(危险,仅在特殊兼容场景使用)

.sessionManagement()
    .sessionFixation().none(); // ❌ 不更换 Session ID,存在攻击风险

完整示例一:电商平台防护会话固定攻击

场景说明

用户访问电商网站时,攻击者发送带 jsessionid 参数的链接给用户。用户点击后使用该 Session ID 浏览商品,随后登录账号。攻击者凭同一 Session ID 访问用户订单和支付页面。

操作前配置(关闭防护,none() 状态)

@EnableWebSecurity
public class MallSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/", "/products/**", "/login").permitAll()
                .antMatchers("/orders/**", "/checkout/**").authenticated()
                .anyRequest().authenticated()
            .and()
            .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/orders")
                .permitAll()
            .and()
            .sessionManagement()
                .sessionFixation().none(); // ❌ 错误:不更换 Session ID
    }
}

攻击流程:

  1. 攻击者访问 https://mall.com/,服务器返回 Set-Cookie: JSESSIONID=attack123;
  2. 攻击者发送链接 https://mall.com/;jsessionid=attack123 给用户;
  3. 用户点击,浏览器使用 JSESSIONID=attack123 浏览商品;
  4. 用户登录成功,Session attack123 绑定到用户 user@example.com;
  5. 攻击者直接访问 https://mall.com/orders,携带 Cookie: JSESSIONID=attack123;
  6. 服务端验证 Session attack123 已认证,返回用户订单列表,攻击成功。

操作后配置(启用默认 migrateSession)

@EnableWebSecurity
public class MallSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/", "/products/**", "/login").permitAll()
                .antMatchers("/orders/**", "/checkout/**").authenticated()
                .anyRequest().authenticated()
            .and()
            .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/orders")
                .permitAll()
            .and()
            .sessionManagement()
                .sessionFixation().migrateSession() // ✅ 更换 Session ID,保留购物车属性
                .maximumSessions(1)                 // 额外加固:同一账号最多 1 个会话
                .maxSessionsPreventsLogin(false);    // 新登录挤掉旧会话
    }
}

结果分析:用户登录时,SessionFixationProtectionStrategy 将 attack123 迁移到新 Session ID newABC789,旧 Session 属性(如购物车)复制到新 Session。攻击者持有的 attack123 在服务端已无认证状态,访问 /orders 时被重定向到登录页,攻击失败。同时 maximumSessions(1) 确保即使攻击者通过其他途径登录,用户新登录会将其挤掉。


完整示例二:金融系统使用 newSession 丢弃匿名数据

场景说明

银行网银系统要求最高安全级别。用户登录前可能在匿名 Session 中留存了一些临时数据(如伪造的转账对象信息)。攻击者可能通过提前向匿名 Session 写入属性(如利用某些页面逻辑漏洞),等用户登录后这些属性随 Session 迁移进入认证会话,造成后续业务风险。

操作前配置(migrateSession 保留匿名属性,风险未根除)

@EnableWebSecurity
public class BankSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/login", "/public/**").permitAll()
                .antMatchers("/transfer/**", "/account/**").hasRole("BANKING")
                .anyRequest().authenticated()
            .and()
            .formLogin()
                .loginPage("/login")
                .permitAll()
            .and()
            .sessionManagement()
                .sessionFixation().migrateSession(); // 属性被保留
    }
}

风险分析:假设网站某公共页面 GET /public/preview 会将请求参数写入 Session(如 session.setAttribute("previewPayee", request.getParameter("payee"))),攻击者诱导用户访问 https://bank.com/public/preview?payee=hacker,该属性存入匿名 Session。用户登录后 migrateSession 将其复制到新 Session,后续用户进入转账页面时,前端可能自动读取 previewPayee 显示为默认收款人,引导用户向错误对象转账。

操作后配置(newSession 彻底隔离)

@EnableWebSecurity
public class BankSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/login", "/public/**").permitAll()
                .antMatchers("/transfer/**", "/account/**").hasRole("BANKING")
                .anyRequest().authenticated()
            .and()
            .formLogin()
                .loginPage("/login")
                .permitAll()
            .and()
            .sessionManagement()
                .sessionFixation().newSession() // ✅ 新建 Session,不保留任何旧属性
                .maximumSessions(1)
                .maxSessionsPreventsLogin(true) // 阻止新登录,直到旧会话过期
                .sessionRegistry(sessionRegistry())
            .and()
            .headers()
                .httpStrictTransportSecurity() // 配合 HSTS 强制 HTTPS
                    .includeSubDomains(true)
                    .maxAgeInSeconds(31536000);
    }

    @Bean
    public SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }
}

结果分析:用户登录后,newSession() 创建全新 Session,匿名阶段的所有属性被清空。攻击者提前写入的 previewPayee 不会带入认证会话。同时 maximumSessions(1).maxSessionsPreventsLogin(true) 确保同一账号不能同时在线,即使攻击者通过其他手段获取了凭据,也无法与用户会话共存。旧 Session 被彻底隔离,攻击链断裂。


易错场景:未启用 Session Fixation 防护时 Session 并发攻击联动

面试高频考点:sessionFixation().none() 与 maximumSessions(1) 是否足够安全?

错误认知:部分开发者认为"只要限制同一账号一个会话,即使不换 Session ID 也没问题"。这是错误的。

场景还原:

.sessionManagement()
    .sessionFixation().none() // 不更换 Session ID
    .maximumSessions(1)       // 同一账号最多 1 个会话
    .maxSessionsPreventsLogin(false); // 新登录挤掉旧会话

攻击流程:

  1. 攻击者获取 Session ID abc123,诱骗用户使用;
  2. 用户登录,abc123 绑定用户身份;
  3. 攻击者用 abc123 访问站点,此时 maximumSessions(1) 检测到同一用户有两个会话(用户浏览器一个、攻击者一个),但 maxSessionsPreventsLogin(false) 只是将旧会话标记过期;
  4. 用户下一次刷新页面时,自己的 Session 反而被挤掉,攻击者的 abc123 成为唯一存活会话;
  5. 攻击者继续使用 abc123 以用户身份操作。

根本问题:maximumSessions 控制的是并发会话数量,而不是会话固定攻击。如果不更换 Session ID,攻击者始终持有用户登录后的有效 Session ID。当用户刷新或被挤掉时,攻击者的会话可能反而被保留(取决于 SessionRegistry 的实现和并发检测时机)。

正确结论:sessionFixation().migrateSession() 或 newSession() 是必须的基础防护,不能依赖 maximumSessions 替代。maximumSessions 是额外加固措施,二者作用维度不同。

面试追问:migrateSession() 和 newSession() 底层如何更换 Session ID?

答:migrateSession() 在 Servlet 3.1+ 环境下优先调用 HttpServletRequest.changeSessionId()(由容器完成 ID 更换,保留所有属性);在不支持该方法的旧容器上,采用 session.invalidate() + request.getSession(true) 创建新 Session,并手动将旧属性复制过去。newSession() 同样调用 invalidate() + getSession(true),但不复制属性,创建空白 Session。


核心类速查

类说明
SessionFixationProtectionStrategy默认 Session Fixation 防护策略,执行 ID 更换
SessionAuthenticationStrategy接口,定义认证成功后对 Session 的操作策略
ChangeSessionIdAuthenticationStrategy仅更换 Session ID(不复制属性,Servlet 3.1+)
RegisterSessionAuthenticationStrategy将新认证 Session 注册到 SessionRegistry
CompositeSessionAuthenticationStrategy组合多个 Session 认证策略同时执行
SessionRegistry / SessionRegistryImpl会话注册表,用于跟踪和管理活跃会话(配合 maximumSessions)
上一页
安全响应头