Skip to content

Spring Session

什么是 Spring Session?

Spring Session 是 Spring 框架提供的一个会话管理解决方案,它抽象了会话存储机制,使得我们可以轻松地将会话数据存储在不同的存储介质中,如 Redis、数据库、MongoDB 等。

IMPORTANT

Spring Session 主要解决了传统 HttpSession 在分布式环境下的局限性,实现了会话数据的集中管理和共享。

为什么需要 Spring Session?

在传统的单体应用中,HttpSession 数据存储在应用服务器的内存中,这在分布式或集群环境下会带来以下问题:

传统 HttpSession 的局限性

WARNING

在上述架构中,每个服务器都维护自己的 Session 数据,用户的请求如果被分发到不同的服务器,会导致会话丢失。

Spring Session 的解决方案

TIP

Spring Session 将会话数据存储在外部存储中,所有应用服务器都从同一个地方读取和写入会话数据,实现了真正的会话共享。

核心概念

1. Session 存储抽象

Spring Session 提供了多种存储实现:

  • Spring Session Data Redis - 基于 Redis 的会话存储
  • Spring Session JDBC - 基于关系数据库的会话存储
  • Spring Session Data MongoDB - 基于 MongoDB 的会话存储
  • Spring Session Hazelcast - 基于 Hazelcast 的会话存储

2. Session 管理流程

实际业务场景

场景 1:电商购物车系统

在电商系统中,用户的购物车信息需要在多个服务器之间共享:

kotlin
@RestController
@RequestMapping("/api/cart")
class CartController {

    /**
     * 添加商品到购物车
     * 使用 Spring Session 自动管理会话数据
     */
    @PostMapping("/add")
    fun addToCart(
        @RequestBody item: CartItem,
        request: HttpServletRequest
    ): ResponseEntity<String> {
        // 获取 HttpSession,Spring Session 会自动处理会话存储
        val session = request.session

        // 从会话中获取购物车,如果不存在则创建新的
        val cart = session.getAttribute("cart") as? MutableList<CartItem>
            ?: mutableListOf()

        // 添加商品到购物车
        cart.add(item)

        // 将更新后的购物车保存到会话中
        // Spring Session 会自动将这些数据同步到 Redis 等存储中
        session.setAttribute("cart", cart)

        return ResponseEntity.ok("商品已添加到购物车")
    }

    /**
     * 获取购物车内容
     */
    @GetMapping
    fun getCart(request: HttpServletRequest): ResponseEntity<List<CartItem>> {
        val session = request.session
        val cart = session.getAttribute("cart") as? List<CartItem>
            ?: emptyList()

        return ResponseEntity.ok(cart)
    }
}

/**
 * 购物车商品数据类
 */
data class CartItem(
    val productId: String,
    val productName: String,
    val price: Double,
    val quantity: Int
)
java
@RestController
@RequestMapping("/api/cart")
public class CartController {

    /**
     * 添加商品到购物车
     */
    @PostMapping("/add")
    public ResponseEntity<String> addToCart(
            @RequestBody CartItem item,
            HttpServletRequest request) {

        HttpSession session = request.getSession();

        @SuppressWarnings("unchecked")
        List<CartItem> cart = (List<CartItem>) session.getAttribute("cart");
        if (cart == null) {
            cart = new ArrayList<>();
        }

        cart.add(item);
        session.setAttribute("cart", cart);

        return ResponseEntity.ok("商品已添加到购物车");
    }

    /**
     * 获取购物车内容
     */
    @GetMapping
    public ResponseEntity<List<CartItem>> getCart(HttpServletRequest request) {
        HttpSession session = request.getSession();

        @SuppressWarnings("unchecked")
        List<CartItem> cart = (List<CartItem>) session.getAttribute("cart");
        if (cart == null) {
            cart = Collections.emptyList();
        }

        return ResponseEntity.ok(cart);
    }
}

场景 2:用户认证和授权

在微服务架构中,用户的认证信息需要在多个服务之间共享:

kotlin
@Service
class UserAuthService {

    /**
     * 用户登录
     * 将用户信息存储在会话中
     */
    fun login(username: String, password: String, request: HttpServletRequest): Boolean {
        // 验证用户凭据(示例)
        if (validateCredentials(username, password)) {
            val session = request.session

            // 创建用户会话信息
            val userSession = UserSession(
                userId = generateUserId(),
                username = username,
                roles = getUserRoles(username),
                loginTime = System.currentTimeMillis()
            )

            // 存储用户会话信息
            // Spring Session 会自动将这些数据持久化到配置的存储中
            session.setAttribute("userSession", userSession)
            session.setAttribute("isAuthenticated", true)

            return true
        }
        return false
    }

    /**
     * 检查用户是否已认证
     */
    fun isAuthenticated(request: HttpServletRequest): Boolean {
        val session = request.session(false) // 不创建新会话
        return session?.getAttribute("isAuthenticated") as? Boolean ?: false
    }

    /**
     * 获取当前用户信息
     */
    fun getCurrentUser(request: HttpServletRequest): UserSession? {
        val session = request.session(false)
        return session?.getAttribute("userSession") as? UserSession
    }

    /**
     * 用户退出登录
     */
    fun logout(request: HttpServletRequest) {
        val session = request.session(false)
        session?.invalidate() // 销毁会话,Spring Session 会自动清理存储中的数据
    }

    private fun validateCredentials(username: String, password: String): Boolean {
        // 实际的用户验证逻辑
        return true // 示例
    }

    private fun generateUserId(): String = UUID.randomUUID().toString()

    private fun getUserRoles(username: String): List<String> {
        // 获取用户角色的逻辑
        return listOf("USER")
    }
}

/**
 * 用户会话信息数据类
 */
data class UserSession(
    val userId: String,
    val username: String,
    val roles: List<String>,
    val loginTime: Long
)

配置 Spring Session

1. 基于 Redis 的配置

首先添加依赖:

kotlin
// build.gradle.kts
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.session:spring-session-data-redis")
    implementation("org.springframework.boot:spring-boot-starter-data-redis")
}

配置类:

kotlin
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 3600) // 会话过期时间1小时
class SessionConfig {

    /**
     * 配置 Redis 连接工厂
     */
    @Bean
    fun redisConnectionFactory(): RedisConnectionFactory {
        val factory = LettuceConnectionFactory()
        factory.hostName = "localhost"
        factory.port = 6379
        factory.database = 0
        return factory
    }

    /**
     * 配置 Redis 模板
     */
    @Bean
    fun redisTemplate(connectionFactory: RedisConnectionFactory): RedisTemplate<String, Any> {
        val template = RedisTemplate<String, Any>()
        template.connectionFactory = connectionFactory
        template.keySerializer = StringRedisSerializer()
        template.valueSerializer = GenericJackson2JsonRedisSerializer()
        return template
    }
}

2. 基于数据库的配置

kotlin
@Configuration
@EnableJdbcHttpSession(
    maxInactiveIntervalInSeconds = 3600,
    tableName = "SPRING_SESSION" // 自定义表名
)
class JdbcSessionConfig {

    /**
     * 配置数据源
     */
    @Bean
    @Primary
    fun dataSource(): DataSource {
        val dataSource = HikariDataSource()
        dataSource.jdbcUrl = "jdbc:mysql://localhost:3306/session_db"
        dataSource.username = "root"
        dataSource.password = "password"
        dataSource.driverClassName = "com.mysql.cj.jdbc.Driver"
        return dataSource
    }

    /**
     * 配置事务管理器
     */
    @Bean
    fun transactionManager(dataSource: DataSource): PlatformTransactionManager {
        return DataSourceTransactionManager(dataSource)
    }
}

3. 应用配置文件

yaml
# application.yml
spring:
  # Redis 配置
  redis:
    host: localhost
    port: 6379
    database: 0
    timeout: 2000ms
    lettuce:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0
        max-wait: -1ms

  # 数据库配置
  datasource:
    url: jdbc:mysql://localhost:3306/session_db
    username: root
    password: password
    driver-class-name: com.mysql.cj.jdbc.Driver

  # Session 配置
  session:
    store-type: redis # 可选:redis, jdbc, mongodb, hazelcast
    timeout: 30m # 会话超时时间
    redis:
      namespace: myapp:session # Redis 键前缀
      flush-mode: on_save # 会话数据刷新模式

高级特性

1. 自定义会话存储

kotlin
/**
 * 自定义会话存储实现
 */
@Component
class CustomSessionRepository : SessionRepository<CustomSession> {

    private val sessionStore = ConcurrentHashMap<String, CustomSession>()

    override fun createSession(): CustomSession {
        val session = CustomSession()
        session.id = UUID.randomUUID().toString()
        session.creationTime = Instant.now()
        session.lastAccessedTime = Instant.now()
        session.maxInactiveInterval = Duration.ofMinutes(30)
        return session
    }

    override fun save(session: CustomSession) {
        sessionStore[session.id] = session
        // 这里可以实现持久化逻辑,如保存到数据库
    }

    override fun findById(id: String): CustomSession? {
        val session = sessionStore[id]
        return if (session?.isExpired != true) {
            session?.apply { lastAccessedTime = Instant.now() }
        } else {
            sessionStore.remove(id)
            null
        }
    }

    override fun deleteById(id: String) {
        sessionStore.remove(id)
    }
}

/**
 * 自定义会话实现
 */
class CustomSession : Session {

    override lateinit var id: String
    override var creationTime: Instant = Instant.now()
    override var lastAccessedTime: Instant = Instant.now()
    override var maxInactiveInterval: Duration = Duration.ofMinutes(30)

    private val attributes = ConcurrentHashMap<String, Any>()

    val isExpired: Boolean
        get() = Instant.now().isAfter(lastAccessedTime.plus(maxInactiveInterval))

    override fun <T> getAttribute(attributeName: String): T? {
        @Suppress("UNCHECKED_CAST")
        return attributes[attributeName] as? T
    }

    override fun getAttributeNames(): Set<String> = attributes.keys

    override fun setAttribute(attributeName: String, attributeValue: Any?) {
        if (attributeValue != null) {
            attributes[attributeName] = attributeValue
        } else {
            attributes.remove(attributeName)
        }
    }

    override fun removeAttribute(attributeName: String) {
        attributes.remove(attributeName)
    }
}

2. 会话事件监听

kotlin
/**
 * 会话事件监听器
 */
@Component
class SessionEventListener {

    private val logger = LoggerFactory.getLogger(SessionEventListener::class.java)

    /**
     * 监听会话创建事件
     */
    @EventListener
    fun handleSessionCreated(event: SessionCreatedEvent) {
        logger.info("会话已创建: sessionId={}", event.sessionId)

        // 可以在这里执行一些初始化操作
        // 例如:记录用户活动、初始化用户偏好设置等
    }

    /**
     * 监听会话销毁事件
     */
    @EventListener
    fun handleSessionDestroyed(event: SessionDestroyedEvent) {
        logger.info("会话已销毁: sessionId={}", event.sessionId)

        // 可以在这里执行清理操作
        // 例如:清理用户相关的缓存、记录用户活动结束时间等

        val session = event.session
        val userSession = session.getAttribute<UserSession>("userSession")
        if (userSession != null) {
            logger.info("用户 {} 的会话已结束", userSession.username)
            // 执行用户特定的清理逻辑
        }
    }

    /**
     * 监听会话过期事件
     */
    @EventListener
    fun handleSessionExpired(event: SessionExpiredEvent) {
        logger.info("会话已过期: sessionId={}", event.sessionId)

        // 处理会话过期的逻辑
        // 例如:通知用户重新登录、清理相关资源等
    }
}

最佳实践

1. 会话数据序列化

IMPORTANT

存储在会话中的对象必须是可序列化的,建议使用简单的数据类型或实现 Serializable 接口。

kotlin
/**
 * 可序列化的用户会话数据
 */
@Serializable
data class SerializableUserSession(
    val userId: String,
    val username: String,
    val roles: List<String>,
    val loginTime: Long,
    val preferences: Map<String, String> = emptyMap()
) : Serializable {
    companion object {
        private const val serialVersionUID = 1L
    }
}

2. 会话安全配置

kotlin
@Configuration
@EnableWebSecurity
class SecurityConfig {

    /**
     * 配置会话管理安全策略
     */
    @Bean
    fun filterChain(http: HttpSecurity): SecurityFilterChain {
        return http
            .sessionManagement { session ->
                session
                    .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                    .maximumSessions(1) // 限制同一用户的最大会话数
                    .maxSessionsPreventsLogin(false) // 新会话会踢掉旧会话
                    .sessionRegistry(sessionRegistry())
            }
            .csrf { it.disable() }
            .build()
    }

    @Bean
    fun sessionRegistry(): SessionRegistry {
        return SessionRegistryImpl()
    }
}

3. 会话数据清理策略

kotlin
@Component
class SessionCleanupService {

    private val logger = LoggerFactory.getLogger(SessionCleanupService::class.java)

    /**
     * 定期清理过期会话数据
     * 每小时执行一次
     */
    @Scheduled(fixedRate = 3600000)
    fun cleanupExpiredSessions() {
        logger.info("开始清理过期会话数据")

        // 实现清理逻辑
        // 例如:删除 Redis 中的过期键、清理数据库中的过期记录等

        logger.info("过期会话数据清理完成")
    }
}

4. 性能优化

TIP

为了提高性能,建议只在会话中存储必要的数据,避免存储大量数据或复杂对象。

kotlin
/**
 * 会话数据管理工具类
 */
@Component
class SessionManager {

    private val redisTemplate: RedisTemplate<String, Any>

    constructor(redisTemplate: RedisTemplate<String, Any>) {
        this.redisTemplate = redisTemplate
    }

    /**
     * 获取会话数据,支持延迟加载
     */
    fun <T> getSessionData(
        request: HttpServletRequest,
        key: String,
        loader: () -> T
    ): T {
        val session = request.session

        // 首先尝试从会话中获取
        session.getAttribute(key)?.let {
            @Suppress("UNCHECKED_CAST")
            return it as T
        }

        // 如果会话中没有,则执行加载逻辑
        val data = loader()
        session.setAttribute(key, data)
        return data
    }

    /**
     * 异步更新会话数据
     */
    @Async
    fun updateSessionDataAsync(sessionId: String, key: String, value: Any) {
        try {
            redisTemplate.opsForHash<String, Any>()
                .put("spring:session:sessions:$sessionId", key, value)
        } catch (e: Exception) {
            // 处理异常
            logger.error("异步更新会话数据失败", e)
        }
    }

    companion object {
        private val logger = LoggerFactory.getLogger(SessionManager::class.java)
    }
}

监控和调试

1. 会话监控

kotlin
@RestController
@RequestMapping("/api/admin/sessions")
class SessionMonitorController {

    private val sessionRegistry: SessionRegistry
    private val redisTemplate: RedisTemplate<String, Any>

    constructor(
        sessionRegistry: SessionRegistry,
        redisTemplate: RedisTemplate<String, Any>
    ) {
        this.sessionRegistry = sessionRegistry
        this.redisTemplate = redisTemplate
    }

    /**
     * 获取当前活跃会话数量
     */
    @GetMapping("/count")
    fun getActiveSessionCount(): ResponseEntity<Map<String, Any>> {
        val activeSessionCount = sessionRegistry.allPrincipals.size

        return ResponseEntity.ok(mapOf(
            "activeSessionCount" to activeSessionCount,
            "timestamp" to System.currentTimeMillis()
        ))
    }

    /**
     * 获取会话详细信息
     */
    @GetMapping("/{sessionId}")
    fun getSessionInfo(@PathVariable sessionId: String): ResponseEntity<Map<String, Any>> {
        val sessionKey = "spring:session:sessions:$sessionId"
        val sessionData = redisTemplate.opsForHash<String, Any>().entries(sessionKey)

        return if (sessionData.isNotEmpty()) {
            ResponseEntity.ok(sessionData)
        } else {
            ResponseEntity.notFound().build()
        }
    }

    /**
     * 强制销毁指定会话
     */
    @DeleteMapping("/{sessionId}")
    fun destroySession(@PathVariable sessionId: String): ResponseEntity<String> {
        val sessionKey = "spring:session:sessions:$sessionId"
        redisTemplate.delete(sessionKey)

        return ResponseEntity.ok("会话已销毁")
    }
}

2. 会话调试工具

kotlin
@Component
class SessionDebugger {

    private val logger = LoggerFactory.getLogger(SessionDebugger::class.java)

    /**
     * 打印会话详细信息
     */
    fun debugSession(request: HttpServletRequest) {
        val session = request.session(false)

        if (session != null) {
            logger.debug("=== 会话调试信息 ===")
            logger.debug("会话ID: {}", session.id)
            logger.debug("创建时间: {}", Date(session.creationTime))
            logger.debug("最后访问时间: {}", Date(session.lastAccessedTime))
            logger.debug("最大不活跃间隔: {} 秒", session.maxInactiveInterval)
            logger.debug("是否为新会话: {}", session.isNew)

            logger.debug("会话属性:")
            session.attributeNames.asSequence().forEach { name ->
                val value = session.getAttribute(name)
                logger.debug("  {} = {}", name, value)
            }
            logger.debug("==================")
        } else {
            logger.debug("当前请求没有关联的会话")
        }
    }

    /**
     * 验证会话一致性
     */
    fun validateSessionConsistency(request: HttpServletRequest): Boolean {
        val session = request.session(false) ?: return false

        return try {
            // 尝试访问会话属性,验证会话是否可用
            session.attributeNames
            session.creationTime
            session.lastAccessedTime
            true
        } catch (e: Exception) {
            logger.error("会话一致性验证失败", e)
            false
        }
    }
}

总结

Spring Session 为我们提供了一个强大而灵活的会话管理解决方案,特别适用于分布式和集群环境。通过本文的学习,您应该能够:

NOTE

  • 理解 Spring Session 解决的核心问题
  • 掌握不同存储方式的配置和使用方法
  • 了解如何在实际业务场景中应用 Spring Session
  • 学会监控和调试会话相关问题

关键要点回顾

  1. 🎯 核心价值:解决分布式环境下的会话共享问题
  2. 🔧 多种存储:支持 Redis、数据库、MongoDB 等多种存储方式
  3. 🚀 易于集成:与 Spring Boot 完美集成,配置简单
  4. 🛡️ 安全可靠:提供完整的会话安全和生命周期管理
  5. 📊 可监控:支持会话监控和调试功能

选择合适的存储方式和配置策略,Spring Session 将为您的应用提供稳定、可扩展的会话管理能力! 🎉