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

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

SessionFixation

定义与作用

Session Fixation(会话固定攻击) 是一种 Web 安全攻击:攻击者先获取一个有效的 Session ID(如通过访问网站获取响应中的 Set-Cookie: JSESSIONID=xxx),然后诱骗目标用户使用该 Session ID 登录。用户登录后,服务器未更换 Session ID,攻击者持有的旧 Session ID 随即拥有已认证用户的全部权限。

没有安全框架时,开发者必须在每个登录成功处理点手动调用 request.getSession().invalidate() 并重新创建 Session,稍有遗漏(如某个登录接口、OAuth2 回调、RememberMe 自动登录)就会造成漏洞。

Spring Security 通过 SessionFixationProtectionStrategy 在所有认证成功路径统一执行会话固定防护,提供三种策略:

策略行为安全性适用场景
migrateSession()登录成功后更换 Session ID,保留原 Session 的所有属性高默认推荐
newSession()登录成功后创建全新 Session,不保留原属性最高不需要保留购物车等临时数据的场景
none()不更换 Session ID,原 Session 继续有效低仅内部可信网络或兼容性要求

核心原理

攻击流程与防护机制

攻击者步骤:
1. 访问目标网站 → 获取响应 Cookie: JSESSIONID=ABC123
2. 向目标用户发送包含恶意链接的邮件/消息,链接中强制携带 JSESSIONID=ABC123
3. 用户点击链接,浏览器使用 ABC123 访问网站并登录
4. 服务器未更换 Session ID,认证信息绑定到 ABC123
5. 攻击者使用 ABC123 直接访问用户资源

Spring Security 防护:
→ UsernamePasswordAuthenticationFilter 认证成功后
→ SessionFixationProtectionStrategy 介入
→ 调用 HttpSessionFixationProtectionStrategy#onAuthentication()
→ 创建新 Session(或更换 ID),将旧 Session 属性复制到新 Session
→ 将 SecurityContext 写入新 Session
→ 旧 Session 被标记无效,攻击者持有的 Session ID 失效

源码级机制

SessionFixationProtectionStrategy 的核心实现逻辑:

// 简化逻辑
public class SessionFixationProtectionStrategy implements SessionAuthenticationStrategy {

    public void onAuthentication(Authentication authentication,
                                  HttpServletRequest request,
                                  HttpServletResponse response) {
        HttpSession session = request.getSession(false);
        if (session != null) {
            // 1. 保存旧 Session 的所有属性
            Map<String, Object> attributes = new HashMap<>();
            Enumeration<String> names = session.getAttributeNames();
            while (names.hasMoreElements()) {
                String name = names.nextElement();
                attributes.put(name, session.getAttribute(name));
            }

            // 2. 使旧 Session 失效
            session.invalidate();

            // 3. 创建新 Session(Tomcat 等容器会生成新 Session ID)
            HttpSession newSession = request.getSession(true);

            // 4. 将旧属性复制到新 Session(migrateSession 模式)
            for (Map.Entry<String, Object> entry : attributes.entrySet()) {
                newSession.setAttribute(entry.getKey(), entry.getValue());
            }
        }
    }
}

三种策略的实现差异

策略实现类关键差异
migrateSession()SessionFixationProtectionStrategy复制旧 Session 属性到新 Session
newSession()SessionFixationProtectionStrategy 的变体不复制属性,全新 Session
none()NullAuthenticatedSessionStrategy什么都不做,直接返回

Session Fixation 攻击与防护流程


示例一:默认 migrateSession 保留购物车数据

场景说明

电商网站用户在未登录时已将商品加入购物车(存储在 Session 中)。用户登录后,需要保留购物车数据,同时更换 Session ID 防范会话固定攻击。

操作前配置

未配置 Session Fixation 策略,Spring Security 默认使用 migrateSession(),但开发者可能误用 none() 导致安全漏洞:

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class UnsafeSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/cart/**", "/product/**").permitAll()
                .antMatchers("/checkout/**").authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .permitAll()
                .and()
            .sessionManagement()
                .sessionFixation().none();  // ❌ 危险!不更换 Session ID
    }
}

操作后配置

使用 migrateSession()(默认行为,显式声明以增强可读性),登录后保留 Session 属性但更换 Session ID:

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class CartSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/cart/**", "/product/**").permitAll()
                .antMatchers("/checkout/**").authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .permitAll()
                .and()
            .sessionManagement()
                .sessionFixation().migrateSession();  // ✅ 更换 ID,保留属性
    }
}

购物车 Service 示例:

@Service
public class CartService {

    public void addToCart(HttpSession session, Long productId, int quantity) {
        @SuppressWarnings("unchecked")
        Map<Long, Integer> cart = (Map<Long, Integer>) session.getAttribute("cart");
        if (cart == null) {
            cart = new HashMap<>();
        }
        cart.put(productId, cart.getOrDefault(productId, 0) + quantity);
        session.setAttribute("cart", cart);
    }

    public Map<Long, Integer> getCart(HttpSession session) {
        @SuppressWarnings("unchecked")
        Map<Long, Integer> cart = (Map<Long, Integer>) session.getAttribute("cart");
        return cart != null ? cart : Collections.emptyMap();
    }
}

结果分析

  • 用户未登录时访问 /cart/add?productId=101,商品存入 HttpSession 的 cart 属性
  • 用户登录成功后,migrateSession() 触发:旧 Session 失效,新 Session 创建,旧 Session 中的 cart 属性被复制到新 Session
  • 用户访问 /checkout 时,购物车数据完整保留,但浏览器 Cookie 中的 JSESSIONID 已更换
  • 攻击者即使获取了登录前的旧 Session ID,也无法访问已认证的会话

示例二:newSession 用于高安全金融场景

场景说明

银行网银系统对安全性要求极高。用户登录后,之前的所有临时 Session 数据(如预填表单、缓存查询)不应保留,彻底斩断任何潜在的属性污染或劫持风险。

操作前配置

使用 migrateSession() 虽然安全,但会将旧 Session 的所有属性(包括可能被污染的属性)复制到新 Session:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .sessionManagement()
            .sessionFixation().migrateSession();
    // 旧 Session 中的恶意属性(如被篡改的 formToken)会被复制到新 Session
}

操作后配置

使用 newSession(),登录成功后创建全新 Session,不保留任何旧属性:

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class BankingSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/banking/public/**").permitAll()
                .antMatchers("/banking/transfer/**", "/banking/account/**")
                    .hasRole("BANKING_USER")
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/banking/login")
                .permitAll()
                .and()
            .sessionManagement()
                .sessionFixation().newSession()  // 全新 Session,不保留旧属性
                .maximumSessions(1)
                .maxSessionsPreventsLogin(false)
                .expiredUrl("/banking/session-expired")
                .and()
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
    }
}

结果分析

  • 用户在登录前的公共页面 /banking/public/rate-query 查询汇率,Session 中可能缓存了查询参数
  • 登录成功后,newSession() 使旧 Session 完全失效,创建全新 Session
  • 新 Session 中只包含 Spring Security 写入的 SecurityContext 和框架必要属性,无任何业务属性残留
  • 攻击者即使通过 XSS 等方式在登录前污染了 Session 属性,登录后这些污染全部被清除
  • 代价:登录前用户操作的临时数据(如未保存的表单)全部丢失,金融场景下此代价可接受

易错场景:自定义登录成功处理器时遗漏 Session Fixation 防护

问题描述

开发者自定义 AuthenticationSuccessHandler 处理登录成功后的重定向,但同时在处理器中手动操作了 Session。结果发现虽然配置了 migrateSession(),但登录后旧 Session 的属性仍然丢失或行为异常。

根本原因

SessionFixationProtectionStrategy 在 UsernamePasswordAuthenticationFilter 的 successfulAuthentication() 方法中执行,位于 AuthenticationSuccessHandler.onAuthenticationSuccess() 之前。如果开发者在 AuthenticationSuccessHandler 中手动调用 request.getSession() 或 session.invalidate(),会与框架的防护策略产生冲突:

  1. 框架先执行 migrateSession():旧 Session 失效,创建新 Session,复制属性
  2. 然后调用 AuthenticationSuccessHandler
  3. 如果处理器中再次调用 session.invalidate(),会销毁刚刚创建的新 Session,导致用户会话丢失

错误代码

public class CustomSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                          HttpServletResponse response,
                                          Authentication authentication) throws IOException {
        // ❌ 错误!在 SuccessHandler 中手动操作 Session
        HttpSession session = request.getSession();
        session.setAttribute("customFlag", "true");
        // 如果这里调用 session.invalidate(),会摧毁 migrateSession 创建的新 Session

        response.sendRedirect("/dashboard");
    }
}

正确做法

在 AuthenticationSuccessHandler 中安全地操作 Session,只读写属性,不要创建或销毁:

public class SafeSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                          HttpServletResponse response,
                                          Authentication authentication) throws IOException {
        // ✅ 正确:只设置属性,不创建/销毁 Session
        // 此时 request.getSession() 返回的是 migrateSession 已经创建好的新 Session
        request.getSession().setAttribute("loginTime", new Date());
        request.getSession().setAttribute("loginIp", request.getRemoteAddr());

        response.sendRedirect("/dashboard");
    }
}

配置:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .formLogin()
            .loginPage("/login")
            .successHandler(new SafeSuccessHandler())  // 自定义处理器
            .permitAll()
            .and()
        .sessionManagement()
            .sessionFixation().migrateSession();  // 框架先执行,处理器后执行
}

面试考点

问:Spring Security 的 Session Fixation 防护在什么时机执行?自定义 AuthenticationSuccessHandler 时需要注意什么?

答:SessionFixationProtectionStrategy 在 UsernamePasswordAuthenticationFilter.successfulAuthentication() 中执行,早于 AuthenticationSuccessHandler.onAuthenticationSuccess() 的调用。框架先完成 Session ID 更换(或新 Session 创建),然后才调用成功处理器。

自定义处理器时禁止调用 session.invalidate() 或手动创建/替换 Session,否则会破坏框架的防护结果。如果需要在登录后写入自定义属性,直接调用 request.getSession() 即可,此时返回的是框架已经处理好的新 Session,安全地设置属性即可。


版本说明:本章节基于 Spring Security 5.x(WebSecurityConfigurerAdapter + antMatchers + @EnableGlobalMethodSecurity(prePostEnabled=true) 风格)。

上一页
会话管理
下一页
会话并发控制