🌷 古之立大事者,不惟有超世之才,亦必有坚忍不拔之志
🎐 个人CSND主页——Micro麦可乐的博客
🐥《Docker实操教程》专栏以最新的Centos版本为基础进行Docker实操教程,入门到实战
🌺《RabbitMQ》专栏19年编写主要介绍使用JAVA开发RabbitMQ的系列教程,从基础知识到项目实战
🌸《设计模式》专栏以实际的生活场景为案例进行讲解,让大家对设计模式有一个更清晰的理解
🌛《开源项目》本专栏主要介绍目前热门的开源项目,带大家快速了解并轻松上手使用
✨《开发技巧》本专栏包含了各种系统的设计原理以及注意事项,并分享一些日常开发的功能小技巧
💕《Jenkins实战》专栏主要介绍Jenkins+Docker的实战教程,让你快速掌握项目CI/CD,是2024年最新的实战教程
🌞《Spring Boot》专栏主要介绍我们日常工作项目中经常应用到的功能以及技巧,代码样例完整
🌞《Spring Security》专栏中我们将逐步深入Spring Security的各个技术细节,带你从入门到精通,全面掌握这一安全技术
如果文章能够给大家带来一定的帮助!欢迎关注、评论互动~
最新Spring Security实战教程(十三)会话管理机制 - 并发控制与会话固定攻击防护
回顾链接:
最新Spring Security实战教程(一)初识Spring Security安全框架
最新Spring Security实战教程(二)表单登录定制到处理逻辑的深度改造
最新Spring Security实战教程(三)Spring Security 的底层原理解析
最新Spring Security实战教程(四)基于内存的用户认证
最新Spring Security实战教程(五)基于数据库的动态用户认证传统RBAC角色模型实战开发
最新Spring Security实战教程(六)最新Spring Security实战教程(六)基于数据库的ABAC属性权限模型实战开发
最新Spring Security实战教程(七)方法级安全控制@PreAuthorize注解的灵活运用
最新Spring Security实战教程(八)Remember-Me实现原理 - 持久化令牌与安全存储方案
最新Spring Security实战教程(九)前后端分离认证实战 - JWT+SpringSecurity无缝整合
最新Spring Security实战教程(十)权限表达式进阶 - 在SpEL在安全控制中的高阶魔法
最新Spring Security实战教程(十一)CSRF攻防实战 - 从原理到防护的最佳实践
最新Spring Security实战教程(十二)CORS安全配置 - 跨域请求的安全边界设定
专栏更新完毕后,博主将会上传所有章节代码到CSDN资源免费给大家下载,如你不想等后续章节代码需提前获取,可以私信或留言!
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) // 超出拒绝后续登录;false 则踢出最早会话
.expiredUrl("/login?expired") // 会话过期后重定向地址
);
return http.build();
}
}
4. 测试验证方案
为了演示本次并发控制与会话固定攻击防护的功能,这里博主借鉴官方的样例代码稍做调整进行演示
❶ 确保引入spring-security-test
<dependencies>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
</dependency>
</dependencies>
❷ 编写配置类
@Configuration
@EnableWebSecurity
public class SessionSecurityConfig {
// 1. 注册Session事件监听器,以使 Spring Security 能够及时了解会话生命周期事件
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http .authorizeHttpRequests((authorizeRequests) -> authorizeRequests
.anyRequest().authenticated()).formLogin(withDefaults())
.sessionManagement(session -> session
.sessionFixation(sf -> sf.migrateSession())
.maximumSessions(1) // 同一用户仅允许 1 个会话
.maxSessionsPreventsLogin(true) // 超出拒绝后续登录;false 则踢出最早会话
.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;
/**
* @Description:
* @Auther: Micro麦可乐
*/
@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)
值以测试第二次登陆是拒绝,还是退出最早会话
第一次登陆请求
测试请求获取数据
再次登陆发起请求返回登陆页
❻ 会话固定攻击模拟
# 获取初始会话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
的并发会话控制与会话固定防护策略,能够为应用构建坚实的会话安全防线。
如果你在实践过程中有任何疑问或更好的扩展思路,欢迎在评论区留言,最后希望大家 一键三连 给博主一点点鼓励!