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
}
}
攻击流程:
- 攻击者访问
https://mall.com/,服务器返回Set-Cookie: JSESSIONID=attack123; - 攻击者发送链接
https://mall.com/;jsessionid=attack123给用户; - 用户点击,浏览器使用
JSESSIONID=attack123浏览商品; - 用户登录成功,Session
attack123绑定到用户user@example.com; - 攻击者直接访问
https://mall.com/orders,携带Cookie: JSESSIONID=attack123; - 服务端验证 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); // 新登录挤掉旧会话
攻击流程:
- 攻击者获取 Session ID
abc123,诱骗用户使用; - 用户登录,
abc123绑定用户身份; - 攻击者用
abc123访问站点,此时maximumSessions(1)检测到同一用户有两个会话(用户浏览器一个、攻击者一个),但maxSessionsPreventsLogin(false)只是将旧会话标记过期; - 用户下一次刷新页面时,自己的 Session 反而被挤掉,攻击者的
abc123成为唯一存活会话; - 攻击者继续使用
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) |