Spring Security 会话管理详解
一、知识概述
会话管理是 Web 应用安全的重要组成部分,Spring Security 提供了完善的会话管理功能,包括会话创建、固定保护、并发控制、超时处理等。理解会话管理机制,对于构建安全可靠的 Web 应用至关重要。
会话管理的核心概念:
- Session:服务端存储的用户状态
- Session ID:会话标识
- Session Fixation:会话固定攻击
- Concurrent Session:并发会话控制
二、知识点详细讲解
2.1 会话生命周期
1. 用户访问应用
↓
2. 服务端创建 Session
↓
3. 返回 Session ID(通过 Cookie)
↓
4. 后续请求携带 Session ID
↓
5. 服务端根据 Session ID 获取 Session
↓
6. 用户登出或会话超时
↓
7. 销毁 Session
2.2 会话管理策略
| 策略 | 说明 |
|---|---|
| always | 总是创建会话 |
| ifRequired | 需要时创建(默认) |
| never | 不创建会话,但使用已有会话 |
| stateless | 完全无状态 |
2.3 会话固定防护
会话固定攻击原理:
1. 攻击者获取 Session ID
2. 诱导用户使用该 Session ID 登录
3. 攻击者使用相同 Session ID 访问
防护策略:
- none:不防护
- migrateSession:登录时创建新会话(默认)
- changeSessionId:只更改 Session ID
2.4 并发会话控制
控制同一用户的会话数量:
- 最大会话数限制
- 后登录踢出先登录
- 先登录阻止后登录
三、代码示例
3.1 基础会话配置
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SessionConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
// 会话创建策略
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
// 会话固定防护
.sessionFixation(fixation -> fixation
.migrateSession()
)
// 最大会话数
.maximumSessions(1)
.maxSessionsPreventsLogin(false)
.expiredUrl("/login?expired")
);
return http.build();
}
}
3.2 会话创建策略
@Configuration
@EnableWebSecurity
public class SessionPolicyConfig {
// 无状态应用(REST API)
@Bean
public SecurityFilterChain statelessFilterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.csrf(csrf -> csrf.disable());
return http.build();
}
// 传统 Web 应用
@Bean
public SecurityFilterChain statefulFilterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
);
return http.build();
}
}
3.3 会话固定防护
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SessionFixationConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
// 创建新会话(Servlet 3.1+ 推荐)
.sessionFixation(fixation -> fixation
.changeSessionId()
)
// 或迁移会话(保留会话属性)
// .sessionFixation(fixation -> fixation
// .migrateSession()
// )
);
return http.build();
}
}
3.4 并发会话控制
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class ConcurrentSessionConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
.maximumSessions(1) // 每个用户最多1个会话
.maxSessionsPreventsLogin(true) // 阻止新登录
.expiredUrl("/login?expired")
.sessionRegistry(sessionRegistry())
);
return http.build();
}
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
}
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
@Controller
public class SessionController {
@Autowired
private SessionRegistry sessionRegistry;
// 获取在线用户数
@GetMapping("/online-users")
@ResponseBody
public int getOnlineUsers() {
return sessionRegistry.getAllPrincipals().size();
}
// 获取在线用户列表
@GetMapping("/online-users/list")
@ResponseBody
public List<String> getOnlineUserList() {
return sessionRegistry.getAllPrincipals().stream()
.filter(p -> !sessionRegistry.getAllSessions(p, false).isEmpty())
.map(p -> ((UserDetails) p).getUsername())
.collect(Collectors.toList());
}
// 强制用户下线
@PostMapping("/kick-user")
@ResponseBody
public String kickUser(String username) {
sessionRegistry.getAllPrincipals().stream()
.filter(p -> ((UserDetails) p).getUsername().equals(username))
.forEach(p -> {
sessionRegistry.getAllSessions(p, false)
.forEach(SessionInformation::expireNow);
});
return "用户 " + username + " 已被强制下线";
}
}
3.5 会话超时配置
# application.yml
server:
servlet:
session:
timeout: 30m # 会话超时时间(默认30分钟)
cookie:
name: SESSION_ID
path: /
http-only: true
secure: false
max-age: 3600
same-site: lax
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.session.*;
@Configuration
public class SessionTimeoutConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
.invalidSessionUrl("/login?invalid")
.maximumSessions(1)
)
// 会话过期处理
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.addLogoutHandler(new SecurityContextLogoutHandler())
);
return http.build();
}
}
3.6 自定义会话监听器
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.stereotype.Component;
import javax.servlet.http.*;
@Component
public class CustomSessionListener implements HttpSessionListener {
private static final Logger log = LoggerFactory.getLogger(CustomSessionListener.class);
@Override
public void sessionCreated(HttpSessionEvent se) {
HttpSession session = se.getSession();
log.info("会话创建: {}", session.getId());
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
HttpSession session = se.getSession();
log.info("会话销毁: {}", session.getId());
}
}
// 注册监听器
@Bean
public ServletListenerRegistrationBean<CustomSessionListener> sessionListener() {
return new ServletListenerRegistrationBean<>(new CustomSessionListener());
}
3.7 会话事件处理
import org.springframework.context.event.EventListener;
import org.springframework.security.authentication.event.*;
import org.springframework.security.web.session.HttpSessionDestroyedEvent;
import org.springframework.stereotype.Component;
@Component
public class SessionEventHandler {
private static final Logger log = LoggerFactory.getLogger(SessionEventHandler.class);
// 登录成功事件
@EventListener
public void onAuthenticationSuccess(AuthenticationSuccessEvent event) {
String username = event.getAuthentication().getName();
log.info("用户登录成功: {}", username);
}
// 登录失败事件
@EventListener
public void onAuthenticationFailure(AbstractAuthenticationFailureEvent event) {
String username = event.getAuthentication().getName();
String reason = event.getException().getMessage();
log.warn("用户登录失败: {} - {}", username, reason);
}
// 会话销毁事件
@EventListener
public void onSessionDestroyed(HttpSessionDestroyedEvent event) {
log.info("会话销毁: {}", event.getId());
}
}
3.8 Session 共享(集群环境)
<!-- Redis Session 存储 -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
public class RedisSessionConfig {
// Session 自动存储到 Redis
}
# application.yml
spring:
session:
store-type: redis
redis:
namespace: spring:session
redis:
host: localhost
port: 6379
3.9 JWT 无状态认证
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class JwtSessionConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 无状态会话
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
3.10 记住我功能
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import javax.sql.DataSource;
@Configuration
public class RememberMeConfig {
@Autowired
private DataSource dataSource;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.rememberMe(remember -> remember
.key("uniqueAndSecret")
.tokenValiditySeconds(86400) // 1天
.rememberMeParameter("remember-me")
.rememberMeCookieName("remember-me")
.tokenRepository(persistentTokenRepository())
);
return http.build();
}
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl();
repository.setDataSource(dataSource);
return repository;
}
}
-- 记住我 Token 表
CREATE TABLE persistent_logins (
username VARCHAR(64) NOT NULL,
series VARCHAR(64) PRIMARY KEY,
token VARCHAR(64) NOT NULL,
last_used TIMESTAMP NOT NULL
);
四、实战应用场景
4.1 单点登录(SSO)
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SsoConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth -> oauth
.loginPage("/login")
.defaultSuccessUrl("/home")
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.maximumSessions(1)
.expiredUrl("/login?expired")
);
return http.build();
}
}
4.2 强制登出
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.stereotype.Service;
@Service
public class SessionService {
@Autowired
private SessionRegistry sessionRegistry;
// 强制用户下线
public void forceLogout(String username) {
sessionRegistry.getAllPrincipals().stream()
.filter(p -> p instanceof UserDetails)
.filter(p -> ((UserDetails) p).getUsername().equals(username))
.forEach(principal -> {
sessionRegistry.getAllSessions(principal, false)
.forEach(SessionInformation::expireNow);
});
}
// 强制所有用户下线
public void forceLogoutAll() {
sessionRegistry.getAllPrincipals()
.forEach(principal -> {
sessionRegistry.getAllSessions(principal, false)
.forEach(SessionInformation::expireNow);
});
}
}
五、总结与最佳实践
会话策略选择
| 场景 | 推荐策略 |
|---|---|
| REST API | STATELESS |
| 传统 Web | IF_REQUIRED |
| 微服务 | JWT + Redis |
最佳实践
-
安全配置:
- 启用会话固定防护
- 配置合理的超时时间
- 使用安全的 Cookie
-
并发控制:
- 根据业务设置最大会话数
- 处理会话过期提示
-
集群环境:
- 使用 Redis 共享 Session
- 配置 Session 复制
会话管理是 Web 安全的重要组成部分,掌握 Spring Security 的会话管理机制,能够构建出安全可靠的应用系统。