(十三)会话管理机制 - 并发控制与会话固定攻击防护
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 进行入侵。
配置样例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain (HttpSecurity http) throws Exception { http .sessionManagement(session -> session .sessionFixation(fix -> fix.migrateSession() ) ) .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 配置样例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public HttpSessionEventPublisher httpSessionEventPublisher () { return new HttpSessionEventPublisher (); } @Bean public SecurityFilterChain filterChain (HttpSecurity http) throws Exception { http .sessionManagement(session -> session .maximumSessions(1 ) .maxSessionsPreventsLogin(true ) .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 1 2 3 4 5 6 <dependencies > <dependency > <groupId > org.springframework.security</groupId > <artifactId > spring-security-test</artifactId > </dependency > </dependencies >
❷ 编写配置类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @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); } }
❸ 编写测试接口 1 2 3 4 5 6 7 @RestController public class HomeController { @GetMapping("/") public String hello () { return "hello" ; } }
❹ 编写测试类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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) 值以测试第二次登录是拒绝还是踢出最早会话。
第一次登录请求
测试请求获取数据
再次登录被拒绝,返回登录页
❻ 会话固定攻击模拟 1 2 3 4 5 6 7 8 9 10 11 curl -I http://localhost:8080/login Set-Cookie: JSESSIONID=12345; Path=/; HttpOnly curl -X POST http://localhost:8080/login \ -H "Cookie: JSESSIONID=12345" \ -d "username=admin&password=123456" Set-Cookie: JSESSIONID=67890; Path=/; HttpOnly
以该 ID 发起登录请求,登录后检查返回的 Set-Cookie 中 Session ID 是否已更换,验证 ID 刷新策略。
5. 会话事件监听器 当会话失效时,可借助会话事件监听器执行相应操作(如日志记录):
1 2 3 4 5 6 7 8 9 10 11 12 @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 的并发会话控制与会话固定防护策略,能够为应用构建坚实的会话安全防线。