跳到主要内容

会话管理

会话(Session)是 Web 应用中跟踪用户状态的重要机制。Spring Security 提供了完善的会话管理功能,包括会话创建策略、并发控制、会话固定攻击防护等。本章将深入讲解会话管理的配置和最佳实践。

会话管理基础

会话的工作原理

HTTP 是无状态协议,每次请求都是独立的。为了在多个请求之间保持用户状态,Web 应用使用会话机制:

  1. 用户首次访问应用时,服务器创建一个 Session 对象
  2. 服务器生成唯一的 Session ID,通过 Cookie 发送给浏览器
  3. 浏览器在后续请求中携带 Session ID Cookie
  4. 服务器根据 Session ID 找到对应的 Session 对象

Spring Security 中的会话

在 Spring Security 中,用户的认证信息默认存储在 Session 中。认证成功后,SecurityContext 被保存到 Session,后续请求从 Session 恢复 SecurityContext

会话创建策略

Spring Security 支持多种会话创建策略,通过 SessionCreationPolicy 配置:

策略类型

策略说明适用场景
IF_REQUIRED仅在需要时创建会话(默认)传统 Web 应用
ALWAYS总是创建会话强制需要会话的应用
NEVER不创建会话,但使用已存在的特殊场景
STATELESS完全不使用会话JWT 认证、REST API

配置会话策略

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
);

return http.build();
}

无状态配置

对于 REST API 或使用 JWT 的应用,应该禁用会话:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.csrf(csrf -> csrf.disable());

return http.build();
}

会话超时配置

配置超时时间

application.yml 中配置会话超时:

server:
servlet:
session:
timeout: 30m # 30 分钟

或者在代码中配置:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
.invalidSessionUrl("/login?expired") # 会话过期跳转
);

return http.build();
}

自定义会话过期处理

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
.invalidSessionStrategy(new CustomInvalidSessionStrategy())
);

return http.build();
}

public class CustomInvalidSessionStrategy implements InvalidSessionStrategy {

@Override
public void onInvalidSessionDetected(HttpServletRequest request,
HttpServletResponse response) throws IOException {

String requestedWith = request.getHeader("X-Requested-With");

if ("XMLHttpRequest".equals(requestedWith)) {
// AJAX 请求返回 JSON
response.setStatus(401);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"error\": \"会话已过期\", \"code\": \"SESSION_EXPIRED\"}");
} else {
// 普通请求重定向
response.sendRedirect("/login?expired");
}
}
}

并发会话控制

限制单用户会话数

防止同一用户在多个设备上同时登录:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
.maximumSessions(1) // 每个用户只能有一个活跃会话
.maxSessionsPreventsLogin(false) // 新登录踢掉旧会话
.expiredUrl("/login?expired") // 被踢下线跳转
);

return http.build();
}

阻止新登录

如果希望阻止新登录而不是踢掉旧会话:

.sessionManagement(session -> session
.maximumSessions(1)
.maxSessionsPreventsLogin(true) // 阻止新登录
.expiredUrl("/login?max-sessions")
)

会话事件监听

要使并发会话控制生效,需要注册 HttpSessionEventPublisher

@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}

自定义会话过期策略

.sessionManagement(session -> session
.maximumSessions(1)
.expiredSessionStrategy(new CustomSessionInformationExpiredStrategy())
)

public class CustomSessionInformationExpiredStrategy
implements SessionInformationExpiredStrategy {

@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event)
throws IOException {

HttpServletRequest request = event.getRequest();
HttpServletResponse response = event.getResponse();

// 记录被踢下线的用户
String username = event.getSessionInformation().getPrincipal().toString();
System.out.println("用户 " + username + " 在其他设备登录,被踢下线");

// 返回响应
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(
"{\"error\": \"您的账号在其他设备登录\", \"code\": \"KICKED_OUT\"}"
);
}
}

会话固定攻击防护

攻击原理

会话固定攻击(Session Fixation)是一种安全漏洞:攻击者获取一个有效的 Session ID,诱导受害者使用这个 Session ID 登录,然后攻击者就可以使用同一个 Session ID 访问受害者的账户。

防护策略

Spring Security 默认启用会话固定攻击防护,支持三种策略:

策略说明
changeSessionId不创建新会话,只改变 Session ID(默认,Servlet 3.1+)
newSession创建全新会话,不复制原有属性
migrateSession创建新会话并复制原有属性(Servlet 3.0 及以下默认)
none禁用防护(不推荐)

配置防护策略

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
.sessionFixation(sessionFixation -> sessionFixation
.changeSessionId() // 推荐
)
);

return http.build();
}

SecurityContext 存储

默认存储机制

Spring Security 默认使用 HttpSessionSecurityContextRepositorySecurityContext 存储在 HTTP Session 中。

自定义存储位置

可以自定义 SecurityContextRepository 来改变存储位置:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.securityContext(securityContext -> securityContext
.securityContextRepository(new CustomSecurityContextRepository())
);

return http.build();
}

public class CustomSecurityContextRepository implements SecurityContextRepository {

private final RedisTemplate<String, SecurityContext> redisTemplate;

@Override
public SecurityContext loadContext(HttpRequestResponseHolder requestResponse) {
String sessionId = getSessionId(requestResponse.getRequest());
if (sessionId != null) {
return redisTemplate.opsForValue().get("security:context:" + sessionId);
}
return SecurityContextHolder.createEmptyContext();
}

@Override
public void saveContext(SecurityContext context, HttpServletRequest request,
HttpServletResponse response) {
String sessionId = getSessionId(request);
if (sessionId != null && context.getAuthentication() != null) {
redisTemplate.opsForValue().set(
"security:context:" + sessionId,
context,
30,
TimeUnit.MINUTES
);
}
}

@Override
public boolean containsContext(HttpServletRequest request) {
String sessionId = getSessionId(request);
return sessionId != null &&
Boolean.TRUE.equals(redisTemplate.hasKey("security:context:" + sessionId));
}

private String getSessionId(HttpServletRequest request) {
HttpSession session = request.getSession(false);
return session != null ? session.getId() : null;
}
}
server:
servlet:
session:
cookie:
name: JSESSIONID # Cookie 名称
path: / # Cookie 路径
domain: example.com # Cookie 域名
max-age: 1800 # 最大存活时间(秒)
http-only: true # HttpOnly 标志
secure: true # Secure 标志(仅 HTTPS)
same-site: lax # SameSite 属性
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login")
.deleteCookies("JSESSIONID") # 删除 Session Cookie
.invalidateHttpSession(true) # 使 Session 失效
);

return http.build();
}

使用 Clear-Site-Data 头

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.logout(logout -> logout
.addLogoutHandler(new HeaderWriterLogoutHandler(
new ClearSiteDataHeaderWriter(ClearSiteDataHeaderWriter.Directive.COOKIES)
))
);

return http.build();
}

分布式会话

问题背景

在集群环境中,用户请求可能被负载均衡到不同的服务器。如果 Session 存储在服务器内存中,用户状态无法在服务器间共享。

Spring Session 解决方案

Spring Session 提供了分布式会话支持,可以将 Session 存储在 Redis、JDBC、Hazelcast 等外部存储中。

添加依赖:

<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置 Redis 存储:

spring:
session:
store-type: redis
data:
redis:
host: localhost
port: 6379

启用 Spring Session:

@Configuration
@EnableRedisHttpSession
public class SessionConfig {

@Bean
public LettuceConnectionFactory connectionFactory() {
return new LettuceConnectionFactory();
}
}

配置会话序列化

@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}

@Bean
public RedisIndexedSessionRepository sessionRepository(
RedisOperations<String, Object> sessionRedisOperations) {

RedisIndexedSessionRepository repository =
new RedisIndexedSessionRepository(sessionRedisOperations);

repository.setDefaultMaxInactiveInterval(Duration.ofMinutes(30));

return repository;
}

会话安全最佳实践

最小化会话使用

  • 无状态 API 不应使用 Session
  • 敏感操作应要求重新认证
  • 及时清理不需要的会话数据

会话安全配置清单

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
// 设置合适的会话创建策略
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)

// 配置会话固定攻击防护
.sessionFixation(sessionFixation -> sessionFixation.changeSessionId())

// 配置并发会话控制
.maximumSessions(1)
.maxSessionsPreventsLogin(false)
.expiredUrl("/login?expired")
)

// 配置安全 Cookie
.sessionManagement(session -> session
.sessionFixation(sessionFixation -> sessionFixation.changeSessionId())
);

return http.build();
}

监控会话状态

@Component
public class SessionMonitoringListener {

@EventListener
public void onSessionCreated(SessionCreatedEvent event) {
String sessionId = event.getSessionId();
System.out.println("会话创建: " + sessionId);
}

@EventListener
public void onSessionDestroyed(SessionDestroyedEvent event) {
String sessionId = event.getId();
SecurityContext context = event.getSecurityContext();

if (context != null && context.getAuthentication() != null) {
String username = context.getAuthentication().getName();
System.out.println("会话销毁: " + sessionId + ", 用户: " + username);
}
}
}

小结

本章详细介绍了会话管理的配置和最佳实践:

  • 会话创建策略决定了应用如何管理会话,无状态 API 应禁用会话
  • 会话超时配置控制会话的生命周期
  • 并发会话控制限制同一用户的活跃会话数
  • 会话固定攻击防护是重要的安全措施
  • 自定义 SecurityContext 存储支持分布式场景
  • Spring Session 解决集群环境下的会话共享问题

正确配置会话管理对于构建安全、可扩展的 Web 应用至关重要。下一章将学习方法级安全控制。