乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Spring Security 基础

    • 本章定位
    • Spring Security 是什么
    • DelegatingFilterProxy
    • 安全过滤器链
    • SecurityFilterChain
    • 过滤器执行顺序
  • 第2章 认证

    • 本章定位
    • Authentication
    • 认证流程
    • AuthenticationManager
    • ProviderManager
    • DaoAuthenticationProvider
    • UserDetails
    • UserDetailsService
    • PasswordEncoder
    • BCryptPasswordEncoder
    • DelegatingPasswordEncoder
    • 表单登录
    • SecurityContext
    • SecurityContextHolder
    • UsernamePasswordAuthenticationToken
  • 第3章 授权

    • 本章定位
    • 授权模型
    • GrantedAuthority
    • AccessDecisionManager
    • AccessDecisionVoter
    • URL 级别授权
    • 方法级别安全
    • @PreAuthorize
    • @PostAuthorize
    • @PreFilter
    • @PostFilter
    • @Secured
    • RoleHierarchy
  • 第4章 过滤器链

    • 本章定位
    • FilterChainProxy
    • SecurityContextHolderFilter
    • LogoutFilter
    • BasicAuthenticationFilter
    • CsrfFilter
    • CorsFilter
    • HeaderWriterFilter
    • AnonymousAuthenticationFilter
    • RequestCacheAwareFilter
    • ExceptionTranslationFilter
    • FilterSecurityInterceptor
  • 第5章 会话管理

    • 本章定位
    • 会话管理
    • SessionFixation
    • 会话并发控制
    • SessionCreationPolicy
    • RememberMe
  • 第6章 JWT

    • 本章定位
    • JWT
    • JwtDecoder
    • JWT 认证
    • JwtAuthenticationConverter
  • 第7章 OAuth2

    • 本章定位
    • OAuth2 基础
    • OAuth2 Client
    • OAuth2 Resource Server
    • 第三方登录配置
  • 第8章 攻击防护

    • 本章定位
    • CSRF 跨站请求伪造防护
    • CORS 跨域防护
    • Clickjacking 点击劫持防护
    • 安全响应头
    • Session Fixation 会话固定防护
  • 第9章 测试

    • 本章定位
    • 安全测试
    • @WithMockUser
    • 最佳实践

会话管理

定义与作用

在 Web 应用中,会话管理(Session Management) 负责跟踪用户从登录到登出的完整交互周期。没有安全框架时,开发者需要手写大量重复代码:手动创建 HttpSession、判断会话是否过期、处理并发登录冲突、防范会话固定攻击等。任何一个环节疏漏,都会直接暴露安全漏洞。

Spring Security 将会话管理内建为核心安全模块,通过 SessionManagementConfigurer 提供统一的 DSL 配置入口,涵盖:

能力说明
会话创建策略控制何时创建 HttpSession(无状态 / 按需 / 始终)
会话超时管理统一配置会话失效时间,过期后自动清理安全上下文
会话注册表维护所有活跃会话的注册中心,支持会话数量统计与强制下线
会话固定防护登录成功后更换 Session ID,防止攻击者劫持旧会话
会话并发控制限制同一账号同时在线的会话数量

没有框架时的手写痛点:

  • 手动在 LoginServlet 中调用 request.getSession(),但不知道应该何时不创建会话
  • 会话超时后,未清理的 SecurityContext 导致匿名用户仍能访问受保护资源
  • 并发登录完全依赖开发者自行维护在线用户表,代码散落在各 Controller 中
  • 会话固定攻击需要手动在登录成功后调用 session.invalidate() 再重建,极易遗漏

核心原理

Spring Security 的会话管理由过滤器链中多个组件协同完成:

HttpSessionSecurityContextRepository  →  从 Session 加载/存储 SecurityContext
SessionManagementFilter              →  检测会话是否过期、并发冲突
SessionFixationProtectionStrategy    →  登录成功后更换 Session ID
ConcurrentSessionFilter              →  拦截并发超限的会话
SessionRegistry / SessionRegistryImpl →  维护所有活跃会话的注册表

会话管理在过滤器链中的位置

顺序过滤器与会话管理的关系
3SecurityContextPersistenceFilter从 HttpSession 读取 SecurityContext 到 SecurityContextHolder;请求结束时将 SecurityContext 写回 HttpSession
9SessionManagementFilter检测会话 ID 变更、会话过期、并发冲突,触发 SessionAuthenticationStrategy
12ConcurrentSessionFilter检查当前会话是否被标记为过期(expired),若过期则强制跳转或销毁

核心类职责

  • SessionManagementConfigurer:配置入口,通过 http.sessionManagement() 开启 DSL 配置
  • SessionCreationPolicy:枚举定义四种会话创建策略(详见独立文档)
  • SessionRegistry:会话注册表接口,SessionRegistryImpl 基于内存实现,存储 SessionInformation(会话 ID、主体、最后请求时间、是否过期标记)
  • SessionAuthenticationStrategy:会话认证策略接口,实现类包括 SessionFixationProtectionStrategy、ConcurrentSessionControlAuthenticationStrategy、RegisterSessionAuthenticationStrategy

会话超时与过期处理

会话超时有两种触发方式:

  1. 容器层超时:web.xml 或 application.properties 中配置 session-timeout
  2. Spring Security 层检测:ConcurrentSessionFilter 定期检查 SessionInformation.isExpired()

当会话过期时,SessionManagementFilter 将保存当前请求到 RequestCache,然后重定向到 expiredUrl 或调用 InvalidSessionStrategy。


会话管理整体流程


示例一:配置会话超时与过期跳转

场景说明

电商后台管理系统的管理员会话闲置 30 分钟后应自动失效,并跳转至会话过期提示页面,避免管理员离开后浏览器被他人利用。

操作前配置

仅配置了登录,未设置会话超时和过期处理,浏览器关闭后会话仍残留,重新打开可能无需重新登录:

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class OldSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .permitAll();
        // 缺少会话超时配置,容器默认 30 分钟,无自定义过期跳转
    }
}

操作后配置

显式配置会话超时时间、过期跳转 URL,以及检测无效会话时的跳转策略:

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SessionSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .permitAll()
                .and()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                .invalidSessionUrl("/session-invalid")   // 检测到无效会话时跳转
                .maximumSessions(1)                       // 同一账号最多 1 个会话
                .maxSessionsPreventsLogin(false)          // 新登录挤掉旧会话
                .expiredUrl("/session-expired")           // 会话被挤掉或过期时跳转
                .sessionRegistry(sessionRegistry())       // 注入注册表
                .and()
                .sessionFixation().migrateSession();      // 登录后更换 Session ID,保留属性
    }

    @Bean
    public SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }
}

结果分析

  • 管理员在 /admin/dashboard 页面闲置 30 分钟后,容器自动销毁 HttpSession
  • 下一次操作触发 SessionManagementFilter 检测到会话无效,重定向至 /session-invalid
  • 页面提示"会话已过期,请重新登录",管理员必须重新输入凭据
  • SessionRegistryImpl 维护在线管理员列表,运维人员可通过 sessionRegistry.getAllPrincipals() 获取当前在线人数

示例二:注册 SessionRegistry 用于在线用户监控

场景说明

金融系统需要实时统计当前在线用户数量,并支持管理员在后台强制踢掉某个用户的所有会话。需要显式暴露 SessionRegistry Bean,以便在 Controller 中注入使用。

操作前配置

未配置 SessionRegistry,虽然设置了并发控制,但 Spring Security 默认使用内部不可见的注册表,外部无法获取在线用户信息:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .sessionManagement()
            .maximumSessions(1)
            .maxSessionsPreventsLogin(false);
    // 没有 sessionRegistry() 调用,无法从外部访问注册表
}

操作后配置

显式声明 SessionRegistryImpl Bean,并在 sessionManagement 中注入,同时在业务层提供查询和强制下线接口:

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class OnlineMonitorConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/admin/online/**").hasRole("ADMIN")
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .and()
            .sessionManagement()
                .maximumSessions(1)
                .maxSessionsPreventsLogin(false)
                .expiredUrl("/session-expired")
                .sessionRegistry(sessionRegistry());  // 显式注入,外部可访问
    }

    @Bean
    public SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }
}

AdminController 在线监控接口:

@RestController
@RequestMapping("/admin/online")
public class OnlineUserController {

    @Autowired
    private SessionRegistry sessionRegistry;

    @GetMapping
    @PreAuthorize("hasRole('ADMIN')")
    public List<Map<String, Object>> listOnlineUsers() {
        List<Map<String, Object>> result = new ArrayList<>();
        for (Object principal : sessionRegistry.getAllPrincipals()) {
            if (principal instanceof UserDetails) {
                UserDetails user = (UserDetails) principal;
                List<SessionInformation> sessions = sessionRegistry.getAllSessions(user, false);
                for (SessionInformation session : sessions) {
                    Map<String, Object> info = new HashMap<>();
                    info.put("username", user.getUsername());
                    info.put("sessionId", session.getSessionId());
                    info.put("lastRequest", session.getLastRequest());
                    info.put("expired", session.isExpired());
                    result.add(info);
                }
            }
        }
        return result;
    }

    @PostMapping("/kick/{sessionId}")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<?> kickUser(@PathVariable String sessionId) {
        SessionInformation session = sessionRegistry.getSessionInformation(sessionId);
        if (session != null) {
            session.expireNow();  // 标记会话过期
            // 下一次该会话请求到达时,ConcurrentSessionFilter 会强制其跳转
        }
        return ResponseEntity.ok().build();
    }
}

结果分析

  • GET /admin/online 返回当前所有在线用户的会话列表,包括用户名、会话 ID、最后请求时间
  • POST /admin/online/kick/{sessionId} 将该会话标记为 expired
  • 被踢用户下一次请求到达 ConcurrentSessionFilter 时,检测到 isExpired() == true,强制跳转至 expiredUrl
  • 未注入 SessionRegistry 时,上述功能完全无法实现,因为默认内部注册表对外不可见

易错场景:会话并发配置不生效

问题描述

配置了 maximumSessions(1) 和 maxSessionsPreventsLogin(false),但同一账号在 Chrome 和 Firefox 中同时登录后,两个浏览器都能正常操作,并未发生挤掉现象。

根本原因

SessionRegistry 未注入配置。ConcurrentSessionControlAuthenticationStrategy 依赖 SessionRegistry 跟踪会话,但 Spring Security 默认不会自动将内部使用的 SessionRegistry 与并发控制共享。如果 DSL 中缺少 .sessionRegistry(sessionRegistry()) 调用,并发策略会使用一个独立的、不可见的注册表,导致业务层注入的 SessionRegistry 与实际并发控制使用的不是同一个对象。

错误配置

@Bean
public SessionRegistry sessionRegistry() {
    return new SessionRegistryImpl();  // 声明了 Bean,但 DSL 中未引用
}

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.sessionManagement()
        .maximumSessions(1)
        .maxSessionsPreventsLogin(false);
    // 缺少 .sessionRegistry(sessionRegistry()) !!!
}

正确配置

@Bean
public SessionRegistry sessionRegistry() {
    return new SessionRegistryImpl();
}

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.sessionManagement()
        .maximumSessions(1)
        .maxSessionsPreventsLogin(false)
        .sessionRegistry(sessionRegistry());  // 必须显式注入同一个 Bean
}

面试考点

问:maximumSessions(1) 配置了但不生效,可能的原因是什么?

答:最常见的原因是未在 sessionManagement() DSL 中调用 .sessionRegistry(sessionRegistry()) 注入注册表。Spring Security 的并发控制策略需要 SessionRegistry 来跟踪当前主体的所有会话,如果未显式注入,并发策略会使用一个内部独立的注册表,导致外部无法感知或控制会话状态,并发限制失效。正确做法是显式声明 SessionRegistryImpl Bean,并在 maximumSessions() 链式调用中通过 .sessionRegistry() 将其注入。


版本说明:本章节基于 Spring Security 5.x(WebSecurityConfigurerAdapter + antMatchers + @EnableGlobalMethodSecurity(prePostEnabled=true) 风格)。

上一页
本章定位
下一页
SessionFixation