34-Spring Security 会话管理详解

6 阅读5分钟

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 APISTATELESS
传统 WebIF_REQUIRED
微服务JWT + Redis

最佳实践

  1. 安全配置

    • 启用会话固定防护
    • 配置合理的超时时间
    • 使用安全的 Cookie
  2. 并发控制

    • 根据业务设置最大会话数
    • 处理会话过期提示
  3. 集群环境

    • 使用 Redis 共享 Session
    • 配置 Session 复制

会话管理是 Web 安全的重要组成部分,掌握 Spring Security 的会话管理机制,能够构建出安全可靠的应用系统。