SpringSecurity实战教程(十三)会话管理机制 - 并发控制与会话固定攻击防护

1408 字
7 分钟
SpringSecurity实战教程(十三)会话管理机制 - 并发控制与会话固定攻击防护

1. 前言#

在 Web 应用安全体系中,会话管理是认证授权后的重要防线。攻击者常通过会话劫持会话固定突破系统边界,而业务系统则面临并发滥用带来的资源风险。

Spring Security 的会话管理模块由 SessionManagementFilter 与一系列 SessionAuthenticationStrategy 共同协作,负责在用户登录或访问受保护资源时执行统一的会话检查与策略。默认情况下,框架允许单个用户拥有无限多个并发会话,而在每次登录时会执行会话固定保护策略,将旧 Session ID 迁移到新 Session 中,以防止攻击者利用已有的 Session ID 进行劫持。

在本章节,笔者将基于 Spring Security 6,深入解析会话管理的安全实践。


2. 会话固定攻击防护原理#

2.1 攻击概述#

会话固定(Session Fixation)指攻击者诱导受害者在已知的 Session ID 下登录,随后持该 ID 进行未授权操作。Spring Security 通过会话固定保护策略,在每次用户登录后刷新 Session ID 来防御此类攻击。

攻击流程解析#

会话固定攻击流程示意图
会话固定攻击流程示意图

2.2 Spring Security 防御策略#

SessionManagementFilter 在认证成功后,会调用相应的 SessionAuthenticationStrategy 执行上述策略。migrateSession() 在底层通过 HttpServletRequest.changeSessionId()复制属性到新 Session 来实现 ID 刷新,有效防止攻击者使用固定 ID 进行入侵。

配置样例#

@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 启用会话固定防护,采用迁移会话策略
.sessionManagement(session -> session
.sessionFixation(fix ->
fix.migrateSession() // 默认:保留旧数据,生成新 Session ID
// fix.changeSessionId() // 仅变更会话 ID
// fix.newSession() // 完全新建 Session,不保留任何属性
// fix.none() // 禁用保护(不推荐)
)
)
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
);
return http.build();
}
}

配置说明#

  • none:不做任何处理,不推荐使用
  • changeSessionId:认证后仅变更会话 ID
  • newSession:创建全新、干净的 Session,不复制原属性
  • migrateSession(默认):创建新 Session 并复制原有属性,保留必要数据同时更换 ID,防范攻击者利用旧 ID

3. 并发会话控制方案#

当同一账号在多处登录时,可能带来数据冲突、授权混乱甚至资源浪费。Spring Security 通过并发会话控制,可限制用户的最大在线会话数,并在超过限制时选择“踢出旧会话”或“拒绝新登录”两种策略。

3.1 为什么要限制并发会话?#

  • 防止账号共享:同一账号在多处登录可能意味着凭证泄露
  • 防范会话劫持:一旦旧会话被劫持,可通过限制数量自动踢出旧会话

3.2 核心组件#

  • SessionRegistry:维护用户与其活跃 Session 的映射
  • ConcurrentSessionControlAuthenticationStrategy:在用户登录时检查并发会话数量,超过上限可抛出 SessionAuthenticationException 或踢出最早会话
  • HttpSessionEventPublisher:侦听 HttpSessionEvent,在 Session 销毁时同步更新 SessionRegistry

3.3 配置样例#

@Configuration
@EnableWebSecurity
public class SecurityConfig {
// 1. 注册 Session 事件监听器,使 Spring Security 能及时感知会话生命周期事件
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
.maximumSessions(1) // 同一用户仅允许 1 个会话
.maxSessionsPreventsLogin(true) // true:超出拒绝后续登录;false:踢出最早会话
.expiredUrl("/login?expired") // 会话过期后重定向地址
);
return http.build();
}
}

4. 测试验证方案#

为演示并发控制与会话固定攻击防护功能,笔者借鉴官方样例代码稍作调整进行演示

官方样例代码:https://github.com/spring-projects/spring-security-samples/tree/main/servlet/spring-boot/java/session-management/maximum-sessions

❶ 确保引入 spring-security-test#

<dependencies>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
</dependency>
</dependencies>

❷ 编写配置类#

@Configuration
@EnableWebSecurity
public class SessionSecurityConfig {
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.formLogin(withDefaults())
.sessionManagement(session -> session
.sessionFixation(sf -> sf.migrateSession())
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
.expiredUrl("/login?expired")
);
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}

❸ 编写测试接口#

@RestController
public class HomeController {
@GetMapping("/")
public String hello() {
return "hello";
}
}

❹ 编写测试类#

import com.toher.springsecurity.demo.session.management.DemoSessionApplication;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@SpringBootTest(classes = DemoSessionApplication.class)
@AutoConfigureMockMvc
public class SessionConcurrencyTest {
@Autowired
private MockMvc mockMvc;
@Test
void testConcurrentLogin() throws Exception {
// 第一次登录
MvcResult mvcResult = mockMvc.perform(formLogin())
.andExpect(authenticated())
.andReturn();
MockHttpSession firstLoginSession = (MockHttpSession) mvcResult.getRequest().getSession();
mockMvc.perform(get("/").session(firstLoginSession))
.andExpect(authenticated());
// 第二次登录将被拒绝
mockMvc.perform(formLogin()).andExpect(authenticated());
// 验证第一次会话已失效
mockMvc.perform(get("/").session(firstLoginSession))
.andExpect(unauthenticated());
}
}

❺ 并行运行测试#

观察控制台信息,可调整 .maxSessionsPreventsLogin(true) 值以测试第二次登录是拒绝还是踢出最早会话。

第一次登录请求

第一次登录截图
第一次登录截图

测试请求获取数据

获取数据截图
获取数据截图

再次登录被拒绝,返回登录页

拒绝登录截图
拒绝登录截图

❻ 会话固定攻击模拟#

Terminal window
# 获取初始会话 ID
curl -I http://localhost:8080/login
Set-Cookie: JSESSIONID=12345; Path=/; HttpOnly
# 使用固定会话 ID 尝试认证
curl -X POST http://localhost:8080/login \
-H "Cookie: JSESSIONID=12345" \
-d "username=admin&password=123456"
# 验证响应是否生成新会话 ID
Set-Cookie: JSESSIONID=67890; Path=/; HttpOnly

以该 ID 发起登录请求,登录后检查返回的 Set-Cookie 中 Session ID 是否已更换,验证 ID 刷新策略。

5. 会话事件监听器#

当会话失效时,可借助会话事件监听器执行相应操作(如日志记录):

@Component
public class SessionActivityListener implements ApplicationListener<SessionDestroyedEvent> {
@Override
public void onApplicationEvent(SessionDestroyedEvent event) {
event.getSecurityContexts().forEach(context -> {
AuditLog.log("会话终止",
context.getAuthentication().getName(),
"原因: " + event.getSessionId());
});
}
}

6. 结语#

通过本章节的讲解与实战示例,结合 sessionFixation().migrateSession()maximumSessions(...) 等配置,相信大家已掌握 Spring Security 的并发会话控制与会话固定防护策略,能够为应用构建坚实的会话安全防线。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
SpringSecurity实战教程(十三)会话管理机制 - 并发控制与会话固定攻击防护
https://fanrich.eu.org/posts/程技/java/springsecurity实战教程/十三会话管理机制---并发控制与会话固定攻击防护/
作者
richfan
发布于
2026-03-02
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
richfan
Hello, I'm richfan.
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:000:00
暂无歌词
分类
标签
站点统计
文章
779
分类
8
标签
138
总字数
3,267,679
运行时长
0
最后活动
0 天前
站点信息
构建平台
Cloudflare Pages
博客版本
Firefly v6.13.5
文章许可
CC BY-NC-SA 4.0

文章目录