Session 管理

Spring Boot 默认使用 Servlet 容器(Tomcat)的 HttpSession,Session 存储在 JVM 内存中。多实例部署时各节点 Session 不共享,通常需要引入 Spring Session 将 Session 集中存储到 Redis 或数据库。


默认 HttpSession

无需额外依赖,直接通过 Servlet API 操作:

@RestController
public class LoginController {
 
    @PostMapping("/login")
    public String login(@RequestBody LoginRequest req, HttpSession session) {
        User user = authService.authenticate(req.username(), req.password());
        session.setAttribute("currentUser", user);
        session.setMaxInactiveInterval(1800);  // 30 分钟不活跃则过期
        return "登录成功";
    }
 
    @GetMapping("/me")
    public User currentUser(HttpSession session) {
        User user = (User) session.getAttribute("currentUser");
        if (user == null) throw new UnauthorizedException("未登录");
        return user;
    }
 
    @PostMapping("/logout")
    public String logout(HttpSession session) {
        session.invalidate();   // 销毁 Session
        return "已退出";
    }
}

配置

server:
  servlet:
    session:
      timeout: 30m            # Session 过期时间
      cookie:
        name: JSESSIONID
        http-only: true       # 禁止 JS 读取,防 XSS
        secure: true          # 仅 HTTPS 传输
        same-site: lax        # 防 CSRF
        path: /

Spring Session(分布式 Session)

Spring Session 透明替换 HttpSession,将数据写入 Redis / JDBC / Hazelcast 等,业务代码无需修改。

依赖(以 Redis 为例)

<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>

配置

spring:
  session:
    store-type: redis           # redis / jdbc / hazelcast / none
    timeout: 30m
    redis:
      namespace: "spring:session"   # Redis key 前缀
      flush-mode: on-save           # on-save(默认)或 immediate
      cleanup-cron: "0 * * * * *"  # 过期 key 清理任务(cron 表达式)
  data:
    redis:
      host: localhost
      port: 6379

开启 Session(Spring Boot 自动配置已包含,显式声明可指定存储类型):

@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
public class SessionConfig {
    // 可在此自定义 RedisSerializer
}

Redis 中存储结构:

spring:session:sessions:{sessionId}          ← Hash,存储 Session 属性
spring:session:sessions:expires:{sessionId}  ← String,用于 TTL 过期
spring:session:expirations:{分钟时间戳}       ← Set,支持定期清理

自定义 Session 序列化

默认使用 JDK 序列化,推荐改为 JSON:

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

存储在 Session 中的对象需要有无参构造函数,以支持 JSON 反序列化。


与 Spring Security 集成

Spring Security 自动与 Spring Session 协作,无需额外配置。Security 的 SecurityContext 会被序列化到 Session:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
 
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/login", "/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .defaultSuccessUrl("/dashboard", true)
            )
            .logout(logout -> logout
                .logoutUrl("/logout")
                .invalidateHttpSession(true)    // 退出时销毁 Session
                .deleteCookies("JSESSIONID")
            )
            // 防止 Session 固定攻击
            .sessionManagement(sm -> sm
                .sessionFixation().migrateSession()  // 登录后更换 Session ID
                .maximumSessions(1)                   // 同一账号最多 1 个会话
                .maxSessionsPreventsLogin(false)      // false:新登录踢出旧会话
            );
        return http.build();
    }
}

并发 Session 控制

限制单账号同时在线数量,需注册 HttpSessionEventPublisher

// 必须注册,Spring Security 才能感知 Session 创建/销毁事件
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
    return new HttpSessionEventPublisher();
}
// Security 配置中
.sessionManagement(sm -> sm
    .maximumSessions(1)
    .maxSessionsPreventsLogin(true)   // true:超出则拒绝新登录
    .expiredUrl("/login?expired=1")   // 被踢出后跳转地址
)

Session 事件监听

监听 Session 的创建、销毁和属性变更:

@Component
public class SessionEventListener
        implements HttpSessionListener, HttpSessionAttributeListener {
 
    @Override
    public void sessionCreated(HttpSessionEvent event) {
        log.info("Session 创建: {}", event.getSession().getId());
    }
 
    @Override
    public void sessionDestroyed(HttpSessionEvent event) {
        log.info("Session 销毁: {}", event.getSession().getId());
        // 可在此清理与该 Session 关联的资源
    }
 
    @Override
    public void attributeAdded(HttpSessionBindingEvent event) {
        log.debug("Session 属性写入: {}", event.getName());
    }
}

手动操作 Session(Spring Session API)

通过 SessionRepository 跨请求直接操作 Session:

@Service
public class SessionAdminService {
 
    private final FindByIndexNameSessionRepository<? extends Session> sessionRepository;
 
    // 查找某用户的所有 Session(需配合 PrincipalNameIndexer)
    public Map<String, ? extends Session> getUserSessions(String username) {
        return sessionRepository.findByPrincipalName(username);
    }
 
    // 强制下线某用户的所有 Session
    public void forceLogout(String username) {
        sessionRepository.findByPrincipalName(username)
            .forEach((id, session) -> sessionRepository.deleteById(id));
    }
}

JDBC 存储(无 Redis 场景)

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-jdbc</artifactId>
</dependency>
spring:
  session:
    store-type: jdbc
    jdbc:
      initialize-schema: always   # 自动建表(生产环境建议 never,手动执行 SQL)
      table-name: SPRING_SESSION

Spring Session 会自动创建 SPRING_SESSIONSPRING_SESSION_ATTRIBUTES 两张表。


Session vs Token 对比

维度Session(服务端状态)JWT Token(客户端状态)
存储位置服务端(内存 / Redis)客户端(Cookie / Header)
吊销能力立即生效(删除 Session)复杂(需黑名单)
水平扩展需共享存储(Redis)天然无状态
数据大小Session ID 极小Token 随 Payload 增大
适用场景Web 应用、传统表单登录前后端分离、移动端、微服务

Token 方案详见 OAuth2与JWT


相关链接

  • Redis集成 — Spring Session Redis 后端配置
  • OAuth2与JWT — 无状态 Token 认证方案
  • 安全 — Spring Security 整体配置
  • 跨域处理 — 跨域场景下 Session Cookie 的 SameSite 配置