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(),会与框架的防护策略产生冲突:
- 框架先执行
migrateSession():旧 Session 失效,创建新 Session,复制属性 - 然后调用
AuthenticationSuccessHandler - 如果处理器中再次调用
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)风格)。