Appearance
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
- 学会监控和调试会话相关问题
关键要点回顾
- 🎯 核心价值:解决分布式环境下的会话共享问题
- 🔧 多种存储:支持 Redis、数据库、MongoDB 等多种存储方式
- 🚀 易于集成:与 Spring Boot 完美集成,配置简单
- 🛡️ 安全可靠:提供完整的会话安全和生命周期管理
- 📊 可监控:支持会话监控和调试功能
选择合适的存储方式和配置策略,Spring Session 将为您的应用提供稳定、可扩展的会话管理能力! 🎉