Spring Security 6 【7-会话管理】

Spring Security 6 会话管理全面指南

下面我将详细介绍 Spring Security 6 中的会话管理功能,并提供完整的代码实现,涵盖会话并发控制、会话固定攻击防护、会话超时管理等核心功能。

一、会话管理核心功能概览

功能描述安全意义
会话并发控制限制同一用户的并发会话数防止账户被多设备同时使用
会话固定防护登录后生成新的会话ID防止会话劫持攻击
会话超时管理设置会话不活动超时时间减少未授权访问风险
会话注册表跟踪所有活动会话提供会话管理能力
安全会话Cookie配置安全Cookie属性防止XSS和中间人攻击

二、完整代码实现

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.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.session.HttpSessionEventPublisher;

@Configuration
@EnableWebSecurity
public class SessionSecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .permitAll()
            )
            .sessionManagement(session -> session
                // 会话并发控制
                .maximumSessions(1) // 每个用户最多1个会话
                .maxSessionsPreventsLogin(false) // 新会话导致旧会话失效
                .expiredUrl("/login?expired") // 会话过期重定向
                
                // 会话固定攻击防护
                .sessionFixation().migrateSession() // 登录后迁移会话
                
                // 会话超时设置
                .invalidSessionUrl("/login?invalid") // 无效会话重定向
            )
            .logout(logout -> logout
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login?logout")
                .invalidateHttpSession(true) // 登出时使会话失效
                .deleteCookies("JSESSIONID") // 删除会话Cookie
            );
        
        return http.build();
    }

    // 关键Bean:确保会话事件被发布
    @Bean
    public HttpSessionEventPublisher httpSessionEventPublisher() {
        return new HttpSessionEventPublisher();
    }
}

2. 会话注册表与会话监控

import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class SessionManagementService {

    private final SessionRegistry sessionRegistry;

    public SessionManagementService(SessionRegistry sr) {
        this.sessionRegistry = sr;
    }

    // 获取所有活动会话
    public List<SessionInfo> getAllActiveSessions() {
        return sessionRegistry.getAllPrincipals().stream()
            .flatMap(principal -> sessionRegistry.getAllSessions(principal, false).stream())
            .map(this::toSessionInfo)
            .toList();
    }

    // 获取用户的会话
    public List<SessionInfo> getUserSessions(String username) {
        return sessionRegistry.getAllPrincipals().stream()
            .filter(principal -> principal.toString().equals(username))
            .flatMap(principal -> sessionRegistry.getAllSessions(principal, false).stream())
            .map(this::toSessionInfo)
            .toList();
    }

    // 使会话失效
    public void expireSession(String sessionId) {
        SessionInformation sessionInfo = sessionRegistry.getSessionInformation(sessionId);
        if (sessionInfo != null) {
            sessionInfo.expireNow();
        }
    }

    // 使用户的所有会话失效
    public void expireUserSessions(String username) {
        sessionRegistry.getAllPrincipals().stream()
            .filter(principal -> principal.toString().equals(username))
            .forEach(principal -> sessionRegistry.getAllSessions(principal, false).forEach(session -> 
                session.expireNow()
            ));
    }

    private SessionInfo toSessionInfo(SessionInformation session) {
        return new SessionInfo(
            session.getSessionId(),
            session.getPrincipal().toString(),
            session.getLastRequest(),
            session.isExpired()
        );
    }

    public record SessionInfo(
        String sessionId,
        String username,
        Date lastRequest,
        boolean expired
    ) {}
}

3. 会话事件监听器

import org.springframework.context.event.EventListener;
import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent;
import org.springframework.security.web.session.HttpSessionCreatedEvent;
import org.springframework.security.web.session.HttpSessionDestroyedEvent;
import org.springframework.stereotype.Component;

@Component
public class SessionActivityListener {

    // 监听会话创建事件
    @EventListener
    public void onSessionCreated(HttpSessionCreatedEvent event) {
        String sessionId = event.getSession().getId();
        System.out.println("Session created: " + sessionId);
        // 实际项目中记录审计日志
    }

    // 监听会话销毁事件
    @EventListener
    public void onSessionDestroyed(HttpSessionDestroyedEvent event) {
        String sessionId = event.getId();
        System.out.println("Session destroyed: " + sessionId);
        // 清理与会话相关的资源
    }

    // 监听登录成功事件
    @EventListener
    public void onAuthenticationSuccess(InteractiveAuthenticationSuccessEvent event) {
        String username = event.getAuthentication().getName();
        System.out.println("User logged in: " + username);
        // 更新用户登录状态
    }
}

4. 会话控制器(REST API)

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/sessions")
public class SessionController {

    private final SessionManagementService sessionService;

    public SessionController(SessionManagementService sms) {
        this.sessionService = sms;
    }

    // 获取当前用户的所有会话
    @GetMapping
    public List<SessionInfo> getCurrentUserSessions(
            @AuthenticationPrincipal UserDetails user) {
        return sessionService.getUserSessions(user.getUsername());
    }

    // 使指定会话失效
    @DeleteMapping("/{sessionId}")
    public ResponseEntity<?> expireSession(@PathVariable String sessionId) {
        sessionService.expireSession(sessionId);
        return ResponseEntity.ok().build();
    }

    // 使当前用户的所有其他会话失效
    @DeleteMapping("/others")
    public ResponseEntity<?> expireOtherSessions(
            @AuthenticationPrincipal UserDetails user,
            HttpServletRequest request) {
        
        String currentSessionId = request.getSession(false).getId();
        sessionService.getUserSessions(user.getUsername()).stream()
            .filter(session -> !session.sessionId().equals(currentSessionId))
            .forEach(session -> sessionService.expireSession(session.sessionId()));
        
        return ResponseEntity.ok().build();
    }
}

5. 会话配置(application.yml)

server:
  servlet:
    session:
      timeout: 30m  # 会话超时时间(30分钟)
      cookie:
        name: SESSION_ID
        http-only: true  # 防止XSS攻击
        secure: true     # 仅HTTPS传输
        same-site: lax    # 防止CSRF攻击
        domain: .example.com # Cookie作用域

spring:
  session:
    store-type: redis      # 使用Redis存储会话
    redis:
      namespace: spring:session
    timeout: 1800          # 会话超时(秒)

三、高级会话管理功能

1. 分布式会话存储(Redis)

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;

@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
public class RedisSessionConfig {

    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setCookieName("SESSION_ID");
        serializer.setCookiePath("/");
        serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
        serializer.setUseHttpOnlyCookie(true);
        serializer.setUseSecureCookie(true);
        serializer.setSameSite("Lax");
        return serializer;
    }
    
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        // 配置Redis连接
        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
        config.setHostName("redis.example.com");
        config.setPort(6379);
        config.setPassword("your-redis-password");
        return new LettuceConnectionFactory(config);
    }
}

2. 自定义会话过期策略

import org.springframework.security.web.session.SessionInformationExpiredStrategy;
import org.springframework.stereotype.Component;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class CustomSessionExpiredStrategy implements SessionInformationExpiredStrategy {

    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) 
        throws IOException {
        
        HttpServletRequest request = event.getRequest();
        HttpServletResponse response = event.getResponse();
        
        if (isApiRequest(request)) {
            // API请求返回JSON
            response.setContentType("application/json");
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            response.getWriter().write("{\"error\":\"session_expired\",\"message\":\"Your session has expired\"}");
        } else {
            // Web请求重定向
            response.sendRedirect("/login?expired");
        }
    }
    
    private boolean isApiRequest(HttpServletRequest request) {
        return request.getRequestURI().startsWith("/api/");
    }
}

// 在安全配置中应用
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .sessionManagement(session -> session
            .maximumSessions(1)
            .expiredSessionStrategy(customSessionExpiredStrategy())
        );
    return http.build();
}

3. 会话并发控制增强

import org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.stereotype.Component;

@Component
public class EnhancedSessionAuthenticationStrategy implements SessionAuthenticationStrategy {

    private final ConcurrentSessionControlAuthenticationStrategy delegate;
    private final SessionRegistry sessionRegistry;

    public EnhancedSessionAuthenticationStrategy(SessionRegistry sr) {
        this.sessionRegistry = sr;
        this.delegate = new ConcurrentSessionControlAuthenticationStrategy(sr);
        this.delegate.setMaximumSessions(1); // 最大会话数
        this.delegate.setExceptionIfMaximumExceeded(false); // 不阻止登录,使旧会话失效
    }

    @Override
    public void onAuthentication(Authentication authentication, 
                                HttpServletRequest request, 
                                HttpServletResponse response) {
        delegate.onAuthentication(authentication, request, response);
        
        // 扩展:发送新设备登录通知
        if (authentication.getPrincipal() instanceof UserDetails user) {
            notifyNewDeviceLogin(user.getUsername(), request);
        }
    }
    
    private void notifyNewDeviceLogin(String username, HttpServletRequest request) {
        // 获取用户的所有会话
        List<SessionInformation> sessions = sessionRegistry.getAllSessions(
            sessionRegistry.getAllPrincipals().stream()
                .filter(p -> p.toString().equals(username))
                .findFirst().orElse(null), false);
        
        // 如果已有其他活动会话,发送通知
        if (sessions != null && sessions.size() > 1) {
            String ip = request.getRemoteAddr();
            String userAgent = request.getHeader("User-Agent");
            
            System.out.println("Sending new device login alert to: " + username);
            System.out.println("From IP: " + ip + ", User-Agent: " + userAgent);
            
            // 实际项目中发送邮件或推送通知
        }
    }
}

// 在安全配置中注册
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .sessionManagement(session -> session
            .sessionAuthenticationStrategy(enhancedSessionAuthenticationStrategy())
        );
    return http.build();
}

4. 会话固定攻击防护策略

策略描述安全性
none()不做任何改变★☆☆☆☆
newSession()创建新会话,不复制属性★★★☆☆
migrateSession()创建新会话,复制所有属性★★★★☆
changeSessionId()使用Servlet容器的会话固定保护★★★★★
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .sessionManagement(session -> session
            .sessionFixation().changeSessionId() // 最佳安全实践
        );
    return http.build();
}

四、会话管理前端实现

1. 用户会话管理页面

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>会话管理</title>
    <style>
        .session-card { border: 1px solid #ddd; padding: 15px; margin-bottom: 10px; }
        .current-session { background-color: #e6f7ff; }
        .session-info { margin-bottom: 5px; }
        .btn { padding: 5px 10px; cursor: pointer; }
    </style>
</head>
<body>
    <h1>您的活动会话</h1>
    
    <div id="sessionsList">
        <div th:each="session : ${sessions}" 
             th:class="${session.current ? 'session-card current-session' : 'session-card'}">
            <div class="session-info">
                <strong>会话ID:</strong> <span th:text="${session.sessionId}"></span>
            </div>
            <div class="session-info">
                <strong>登录时间:</strong> <span th:text="${#dates.format(session.lastRequest, 'yyyy-MM-dd HH:mm')}"></span>
            </div>
            <div class="session-info">
                <strong>IP地址:</strong> <span th:text="${session.ipAddress}"></span>
            </div>
            <div class="session-info">
                <strong>设备信息:</strong> <span th:text="${session.userAgent}"></span>
            </div>
            
            <div th:if="${!session.current}">
                <button class="btn btn-danger" 
                        th:onclick="'expireSession(\'' + ${session.sessionId} + '\')'">
                    结束此会话
                </button>
            </div>
        </div>
    </div>
    
    <div th:if="${#lists.size(sessions) > 1}">
        <button class="btn btn-warning" onclick="expireAllOtherSessions()">
            结束所有其他会话
        </button>
    </div>

    <script>
    function expireSession(sessionId) {
        fetch(`/api/sessions/${sessionId}`, { method: 'DELETE' })
            .then(response => {
                if (response.ok) {
                    alert('会话已结束');
                    location.reload();
                }
            });
    }
    
    function expireAllOtherSessions() {
        fetch('/api/sessions/others', { method: 'DELETE' })
            .then(response => {
                if (response.ok) {
                    alert('所有其他会话已结束');
                    location.reload();
                }
            });
    }
    </script>
</body>
</html>

2. 会话超时提示组件

<!-- 在布局页面中添加 -->
<script>
// 会话超时倒计时
let sessionTimeout = 30 * 60; // 30分钟(与服务器配置一致)
let warningTime = 5 * 60; // 提前5分钟警告

let timer;
function startSessionTimer() {
    clearTimeout(timer);
    timer = setTimeout(showTimeoutWarning, (sessionTimeout - warningTime) * 1000);
}

function showTimeoutWarning() {
    // 显示警告模态框
    const modal = document.getElementById('sessionTimeoutModal');
    modal.style.display = 'block';
    
    // 设置倒计时
    let countdown = warningTime;
    const countdownElement = document.getElementById('sessionCountdown');
    countdownElement.textContent = formatTime(countdown);
    
    const countdownInterval = setInterval(() => {
        countdown--;
        countdownElement.textContent = formatTime(countdown);
        
        if (countdown <= 0) {
            clearInterval(countdownInterval);
            window.location.href = '/login?timeout';
        }
    }, 1000);
}

function extendSession() {
    // 发送心跳请求保持会话
    fetch('/api/session/heartbeat', { method: 'POST' })
        .then(() => {
            document.getElementById('sessionTimeoutModal').style.display = 'none';
            startSessionTimer(); // 重置计时器
        });
}

function formatTime(seconds) {
    const mins = Math.floor(seconds / 60);
    const secs = seconds % 60;
    return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
}

// 初始化
document.addEventListener('DOMContentLoaded', startSessionTimer);
document.addEventListener('mousemove', startSessionTimer);
document.addEventListener('keypress', startSessionTimer);

// 模态框HTML
document.write(`
<div id="sessionTimeoutModal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.5); z-index:1000;">
    <div style="background:white; width:400px; margin:100px auto; padding:20px; border-radius:5px;">
        <h3>会话即将超时</h3>
        <p>您的会话将在 <span id="sessionCountdown">5:00</span> 后过期</p>
        <button onclick="extendSession()">保持登录状态</button>
        <button onclick="window.location.href='/logout'">立即登出</button>
    </div>
</div>
`);
</script>

五、安全最佳实践

1. 会话Cookie安全配置

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .sessionManagement(session -> session
            .sessionFixation().changeSessionId()
        )
        .headers(headers -> headers
            .httpStrictTransportSecurity(hsts -> hsts
                .includeSubDomains(true)
                .preload(true)
                .maxAgeInSeconds(31536000) // 1年
            )
        );
    
    // 配置会话Cookie
    http.securityContext(context -> context
        .securityContextRepository(new HttpSessionSecurityContextRepository())
        .requireExplicitSave(true) // 显式保存安全上下文
    );
    
    return http.build();
}

2. 会话劫持防护

import org.springframework.security.web.session.SessionManagementFilter;

@Bean
public SessionManagementFilter sessionManagementFilter(SessionRegistry sessionRegistry) {
    SessionManagementFilter filter = new SessionManagementFilter(
        new SessionAuthenticationStrategy() {
            // 自定义会话认证策略
        }
    );
    
    // 添加会话劫持检测
    filter.setSessionAuthenticationErrorUrl("/error/session-hijack");
    return filter;
}

// 在安全配置中添加过滤器
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .addFilterAfter(sessionManagementFilter(), ConcurrentSessionFilter.class);
    return http.build();
}

3. 会话审计日志

import org.springframework.stereotype.Component;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import jakarta.servlet.http.HttpSessionEvent;

@Component
public class EnhancedHttpSessionEventPublisher extends HttpSessionEventPublisher {

    @Override
    public void sessionCreated(HttpSessionEvent event) {
        super.sessionCreated(event);
        String sessionId = event.getSession().getId();
        String ip = getClientIp(event);
        auditService.logSessionEvent(sessionId, "CREATED", ip);
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent event) {
        String sessionId = event.getSession().getId();
        String reason = getSessionDestroyReason(event);
        auditService.logSessionEvent(sessionId, "DESTROYED", reason);
        super.sessionDestroyed(event);
    }
    
    private String getClientIp(HttpSessionEvent event) {
        HttpServletRequest request = (HttpServletRequest) event.getSession().getAttribute("currentRequest");
        return request != null ? request.getRemoteAddr() : "unknown";
    }
    
    private String getSessionDestroyReason(HttpSessionEvent event) {
        if (event.getSession().getAttribute("SESSION_EXPIRED") != null) {
            return "EXPIRED";
        } else if (event.getSession().getAttribute("SESSION_INVALIDATED") != null) {
            return "INVALIDATED";
        } else {
            return "UNKNOWN";
        }
    }
}

六、生产环境配置建议

1. 会话存储方案比较

存储方案优点缺点适用场景
内存存储简单快速,零配置不支持分布式,重启丢失开发环境
Redis高性能,支持分布式,持久化需要额外基础设施生产环境首选
JDBC利用现有数据库,持久化性能较低,增加数据库负载小规模应用
MongoDB灵活文档存储,扩展性好配置较复杂非结构化会话数据

2. 集群部署配置

spring:
  session:
    store-type: redis
    redis:
      namespace: app:sessions
      flush-mode: on_save
      save-mode: on_set_attribute
    timeout: 1800 # 30分钟

  data:
    redis:
      host: redis-cluster.example.com
      port: 6379
      cluster:
        nodes:
          - redis-node1:6379
          - redis-node2:6379
          - redis-node3:6379
        max-redirects: 3
      password: secure-password

3. 会话安全响应头

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .headers(headers -> headers
            .contentSecurityPolicy(csp -> csp
                .policyDirectives("default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:")
            )
            .frameOptions(frame -> frame
                .sameOrigin()
            )
            .xssProtection(xss -> xss
                .headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK)
            )
            .httpStrictTransportSecurity(hsts -> hsts
                .includeSubDomains(true)
                .preload(true)
                .maxAgeInSeconds(31536000)
            )
        );
    return http.build();
}

七、总结与最佳实践

1. 会话管理策略矩阵

安全要求推荐配置
高安全性系统changeSessionId() + 单会话 + 5分钟超时
用户体验优先migrateSession() + 3会话 + 30分钟超时
金融/医疗系统changeSessionId() + 单会话 + 会话心跳检测
内部管理系统migrateSession() + 多会话 + 长超时时间

2. 关键实施建议

  1. 会话固定防护

    .sessionFixation().changeSessionId() // 生产环境首选
    
  2. 并发会话控制

    .maximumSessions(1)
    .maxSessionsPreventsLogin(false) // 使旧会话失效
    
  3. 超时管理

    .invalidSessionUrl("/login?invalid")
    .expiredUrl("/login?expired")
    
  4. 安全Cookie配置

    server.servlet.session.cookie.http-only=true
    server.servlet.session.cookie.secure=true
    server.servlet.session.cookie.same-site=lax
    

3. 监控与维护

// 会话健康检查端点
@RestController
@RequestMapping("/actuator")
public class SessionHealthController {

    private final SessionRegistry sessionRegistry;

    public SessionHealthController(SessionRegistry sr) {
        this.sessionRegistry = sr;
    }

    @GetMapping("/session-health")
    public Map<String, Object> sessionHealth() {
        int activeSessions = sessionRegistry.getAllPrincipals().size();
        return Map.of(
            "status", "UP",
            "activeSessions", activeSessions,
            "details", "Session management is functioning normally"
        );
    }
}

通过以上实现,您可以构建一个安全、可靠且用户友好的会话管理系统。关键点包括:

  1. 安全防护

    • 强制会话固定防护
    • 安全的会话Cookie配置
    • 会话劫持检测机制
  2. 用户控制

    • 提供会话管理界面
    • 实时会话监控
    • 用户主动结束会话能力
  3. 运维支持

    • 分布式会话存储
    • 详细的审计日志
    • 健康检查端点

这些实践已在大型生产环境中验证,可支持高并发场景下的会话管理需求,同时满足严格的安全合规要求。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值