Skip to content

Spring Boot Actuator Sessions 端点详解 🔐

什么是 Sessions 端点?

Spring Boot Actuator 的 sessions 端点是一个专门用于管理和监控应用程序 HTTP 会话的强大工具。它基于 Spring Session 框架,为开发者提供了查看、管理用户会话的能力。

NOTE

Sessions 端点只有在应用程序集成了 Spring Session 时才会自动启用。Spring Session 是 Spring 生态系统中用于管理用户会话的专业框架。

为什么需要 Sessions 端点? 🤔

传统会话管理的痛点

在没有专业会话管理工具之前,开发者面临以下挑战:

kotlin
@RestController
class UserController {
    
    // 问题1:无法查看当前活跃的会话
    @GetMapping("/login")
    fun login(request: HttpServletRequest): String {
        val session = request.getSession(true)
        session.setAttribute("user", "alice")
        // 开发者无法知道系统中有多少个活跃会话
        return "登录成功"
    }
    
    // 问题2:无法主动管理会话
    @GetMapping("/admin/sessions")
    fun getSessions(): String {
        // 传统方式无法获取所有会话信息
        return "无法实现"
    }
    
    // 问题3:无法强制踢出用户
    @DeleteMapping("/admin/kick-user")
    fun kickUser(@RequestParam userId: String): String {
        // 无法根据用户ID找到并删除对应的会话
        return "无法实现"
    }
}
kotlin
@RestController
class SessionManagementController {
    
    @Autowired
    private lateinit var restTemplate: RestTemplate
    
    // 解决方案1:可以查看指定用户的所有会话
    @GetMapping("/admin/user-sessions")
    fun getUserSessions(@RequestParam username: String): ResponseEntity<String> {
        val url = "http://localhost:8080/actuator/sessions?username=$username"
        return restTemplate.getForEntity(url, String::class.java) 
    }
    
    // 解决方案2:可以查看特定会话详情
    @GetMapping("/admin/session/{sessionId}")
    fun getSessionDetail(@PathVariable sessionId: String): ResponseEntity<String> {
        val url = "http://localhost:8080/actuator/sessions/$sessionId"
        return restTemplate.getForEntity(url, String::class.java) 
    }
    
    // 解决方案3:可以强制删除会话(踢出用户)
    @DeleteMapping("/admin/session/{sessionId}")
    fun kickUser(@PathVariable sessionId: String): ResponseEntity<Void> {
        val url = "http://localhost:8080/actuator/sessions/$sessionId"
        restTemplate.delete(url) 
        return ResponseEntity.ok().build()
    }
}

Sessions 端点解决的核心问题

  1. 会话可视化 - 让"看不见"的会话变得"看得见"
  2. 会话管理 - 提供主动管理会话的能力
  3. 安全控制 - 支持强制用户下线等安全操作
  4. 运维监控 - 帮助运维人员了解系统会话状态

Sessions 端点的核心功能 ⚡

1. 获取用户会话列表

实际应用示例

kotlin
@Configuration
@EnableSpringHttpSession
class SessionConfig {
    
    /**
     * 配置会话存储方式
     * 这里使用内存存储,生产环境建议使用 Redis
     */
    @Bean
    fun sessionRepository(): MapSessionRepository {
        return MapSessionRepository(ConcurrentHashMap())
    }
    
    /**
     * 配置会话超时时间
     */
    @Bean
    fun sessionTimeout(): Duration {
        return Duration.ofMinutes(30) // 30分钟超时
    }
}
kotlin
@RestController
@RequestMapping("/api/auth")
class AuthController {
    
    @PostMapping("/login")
    fun login(
        @RequestParam username: String,
        @RequestParam password: String,
        request: HttpServletRequest
    ): ResponseEntity<Map<String, Any>> {
        
        // 模拟用户验证
        if (isValidUser(username, password)) {
            val session = request.getSession(true)
            
            // 在会话中存储用户信息
            session.setAttribute("username", username) 
            session.setAttribute("loginTime", System.currentTimeMillis()) 
            session.setAttribute("role", getUserRole(username)) 
            
            return ResponseEntity.ok(mapOf(
                "message" to "登录成功",
                "sessionId" to session.id,
                "username" to username
            ))
        }
        
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
            .body(mapOf("error" to "用户名或密码错误"))
    }
    
    private fun isValidUser(username: String, password: String): Boolean {
        // 实际项目中这里会查询数据库验证用户
        return username == "alice" && password == "password123"
    }
    
    private fun getUserRole(username: String): String {
        return if (username == "alice") "ADMIN" else "USER"
    }
}

查询会话的 HTTP 请求示例

bash
# 查询用户 alice 的所有会话
curl 'http://localhost:8080/actuator/sessions?username=alice' \
  -H 'Accept: application/json'

响应结构解析

json
{
  "sessions": [
    {
      "id": "220c01e4-fb37-404f-ba6e-93f9d9c1fed4",
      "attributeNames": ["username", "loginTime", "role"],
      "creationTime": "2025-05-22T08:03:34.888566190Z",
      "lastAccessedTime": "2025-05-22T20:02:49.888573274Z",
      "maxInactiveInterval": 1800,
      "expired": false
    }
  ]
}

TIP

maxInactiveInterval 的单位是秒,1800 秒 = 30 分钟。这意味着如果用户在 30 分钟内没有任何活动,会话将自动过期。

2. 获取单个会话详情

当你需要查看特定会话的详细信息时:

kotlin
@RestController
@RequestMapping("/api/admin")
class SessionAdminController {
    
    @Autowired
    private lateinit var restTemplate: RestTemplate
    
    /**
     * 获取会话详细信息
     * 通常用于管理员查看用户会话状态
     */
    @GetMapping("/session/{sessionId}")
    fun getSessionDetail(@PathVariable sessionId: String): ResponseEntity<SessionInfo> {
        try {
            val actuatorUrl = "http://localhost:8080/actuator/sessions/$sessionId"
            val response = restTemplate.getForEntity(actuatorUrl, SessionInfo::class.java)
            
            return ResponseEntity.ok(response.body)
        } catch (e: Exception) {
            return ResponseEntity.notFound().build()
        }
    }
}

/**
 * 会话信息数据类
 */
data class SessionInfo(
    val id: String,
    val attributeNames: List<String>,
    val creationTime: String,
    val lastAccessedTime: String,
    val maxInactiveInterval: Int,
    val expired: Boolean
)

3. 删除会话(强制用户下线)

这是一个非常实用的功能,特别适用于以下场景:

使用场景

  • 发现可疑登录活动时强制用户下线
  • 用户忘记退出,需要管理员帮助清理会话
  • 系统维护时需要清空所有活跃会话
  • 用户账户被盗用时的紧急处理
kotlin
@RestController
@RequestMapping("/api/admin")
class SecurityController {
    
    @Autowired
    private lateinit var restTemplate: RestTemplate
    
    /**
     * 强制用户下线
     * 删除指定的会话,用户需要重新登录
     */
    @DeleteMapping("/kick-user/{sessionId}")
    fun kickUser(
        @PathVariable sessionId: String,
        @RequestParam reason: String = "管理员操作"
    ): ResponseEntity<Map<String, String>> {
        
        return try {
            // 调用 Actuator 端点删除会话
            val actuatorUrl = "http://localhost:8080/actuator/sessions/$sessionId"
            restTemplate.delete(actuatorUrl) 
            
            // 记录操作日志
            logSecurityAction("KICK_USER", sessionId, reason) 
            
            ResponseEntity.ok(mapOf(
                "message" to "用户已被强制下线",
                "sessionId" to sessionId,
                "reason" to reason
            ))
        } catch (e: Exception) {
            ResponseEntity.badRequest().body(mapOf(
                "error" to "操作失败",
                "details" to (e.message ?: "未知错误")
            ))
        }
    }
    
    /**
     * 批量清理过期会话
     */
    @PostMapping("/cleanup-sessions")
    fun cleanupExpiredSessions(@RequestParam username: String): ResponseEntity<Map<String, Any>> {
        val expiredSessions = mutableListOf<String>()
        
        try {
            // 获取用户的所有会话
            val sessionsUrl = "http://localhost:8080/actuator/sessions?username=$username"
            val response = restTemplate.getForEntity(sessionsUrl, SessionsResponse::class.java)
            
            response.body?.sessions?.forEach { session ->
                if (session.expired) {
                    // 删除过期的会话
                    restTemplate.delete("http://localhost:8080/actuator/sessions/${session.id}")
                    expiredSessions.add(session.id)
                }
            }
            
            return ResponseEntity.ok(mapOf(
                "message" to "清理完成",
                "cleanedSessions" to expiredSessions.size,
                "sessionIds" to expiredSessions
            ))
        } catch (e: Exception) {
            return ResponseEntity.badRequest().body(mapOf(
                "error" to "清理失败",
                "details" to (e.message ?: "未知错误")
            ))
        }
    }
    
    private fun logSecurityAction(action: String, sessionId: String, reason: String) {
        // 实际项目中这里会写入安全日志
        println("SECURITY_LOG: $action - SessionID: $sessionId - Reason: $reason - Time: ${System.currentTimeMillis()}")
    }
}

data class SessionsResponse(
    val sessions: List<SessionInfo>
)

实际业务场景应用 🎯

场景1:在线用户管理系统

kotlin
@RestController
@RequestMapping("/api/admin/users")
class OnlineUserController {
    
    @Autowired
    private lateinit var restTemplate: RestTemplate
    
    /**
     * 获取在线用户列表
     * 管理员可以看到当前所有在线用户
     */
    @GetMapping("/online")
    fun getOnlineUsers(): ResponseEntity<List<OnlineUser>> {
        val onlineUsers = mutableListOf<OnlineUser>()
        
        // 这里需要遍历所有可能的用户名
        // 实际项目中可能需要从数据库获取用户列表
        val usernames = listOf("alice", "bob", "charlie") // 示例用户名
        
        usernames.forEach { username ->
            try {
                val url = "http://localhost:8080/actuator/sessions?username=$username"
                val response = restTemplate.getForEntity(url, SessionsResponse::class.java)
                
                response.body?.sessions?.forEach { session ->
                    if (!session.expired) {
                        onlineUsers.add(OnlineUser(
                            username = username,
                            sessionId = session.id,
                            loginTime = session.creationTime,
                            lastActivity = session.lastAccessedTime,
                            timeoutIn = session.maxInactiveInterval
                        ))
                    }
                }
            } catch (e: Exception) {
                // 用户没有活跃会话,跳过
            }
        }
        
        return ResponseEntity.ok(onlineUsers)
    }
}

data class OnlineUser(
    val username: String,
    val sessionId: String,
    val loginTime: String,
    val lastActivity: String,
    val timeoutIn: Int
)

场景2:安全监控和异常检测

kotlin
@Component
class SessionSecurityMonitor {
    
    @Autowired
    private lateinit var restTemplate: RestTemplate
    
    /**
     * 定期检查可疑会话
     * 例如:同一用户有多个活跃会话可能表示账户被盗用
     */
    @Scheduled(fixedRate = 300000) // 每5分钟检查一次
    fun checkSuspiciousSessions() {
        val suspiciousUsers = mutableListOf<String>()
        val usernames = getUsernamesFromDatabase() // 从数据库获取用户列表
        
        usernames.forEach { username ->
            try {
                val url = "http://localhost:8080/actuator/sessions?username=$username"
                val response = restTemplate.getForEntity(url, SessionsResponse::class.java)
                
                val activeSessions = response.body?.sessions?.filter { !it.expired } ?: emptyList()
                
                // 如果用户有超过2个活跃会话,标记为可疑
                if (activeSessions.size > 2) { 
                    suspiciousUsers.add(username)
                    
                    // 发送安全警报
                    sendSecurityAlert(username, activeSessions.size) 
                    
                    // 可选:自动踢出除最新会话外的所有会话
                    autoCleanupOldSessions(username, activeSessions) 
                }
            } catch (e: Exception) {
                // 处理异常
            }
        }
        
        if (suspiciousUsers.isNotEmpty()) {
            println("发现可疑用户: $suspiciousUsers")
        }
    }
    
    private fun getUsernamesFromDatabase(): List<String> {
        // 实际项目中从数据库查询
        return listOf("alice", "bob", "charlie")
    }
    
    private fun sendSecurityAlert(username: String, sessionCount: Int) {
        // 发送邮件、短信或推送通知
        println("SECURITY ALERT: 用户 $username$sessionCount 个活跃会话")
    }
    
    private fun autoCleanupOldSessions(username: String, sessions: List<SessionInfo>) {
        // 保留最新的会话,删除其他的
        val sortedSessions = sessions.sortedByDescending { it.lastAccessedTime }
        
        sortedSessions.drop(1).forEach { session ->
            try {
                restTemplate.delete("http://localhost:8080/actuator/sessions/${session.id}")
                println("自动清理用户 $username 的旧会话: ${session.id}")
            } catch (e: Exception) {
                println("清理会话失败: ${e.message}")
            }
        }
    }
}

配置和最佳实践 🛠️

1. 启用 Sessions 端点

kotlin
# application.yml
management:
  endpoints:
    web:
      exposure:
        include: "sessions"  # 只暴露 sessions 端点
        # include: "*"       # 或者暴露所有端点(不推荐生产环境)
  endpoint:
    sessions:
      enabled: true

# Spring Session 配置
spring:
  session:
    store-type: redis  # 生产环境推荐使用 Redis
    timeout: 30m       # 会话超时时间

2. 安全配置

IMPORTANT

Sessions 端点包含敏感信息,必须进行适当的安全配置!

kotlin
@Configuration
@EnableWebSecurity
class ActuatorSecurityConfig {
    
    @Bean
    fun actuatorSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
        return http
            .requestMatcher(EndpointRequest.toAnyEndpoint())
            .authorizeHttpRequests { requests ->
                requests
                    .requestMatchers(EndpointRequest.to("sessions")) 
                    .hasRole("ADMIN") // 只有管理员可以访问 sessions 端点
                    .anyRequest().authenticated()
            }
            .httpBasic(Customizer.withDefaults())
            .build()
    }
}

3. 生产环境配置建议

kotlin
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
class RedisSessionConfig {
    
    @Bean
    fun redisConnectionFactory(): RedisConnectionFactory {
        val factory = LettuceConnectionFactory()
        factory.hostName = "localhost"
        factory.port = 6379
        factory.database = 0
        return factory
    }
    
    @Bean
    fun redisTemplate(): RedisTemplate<String, Any> {
        val template = RedisTemplate<String, Any>()
        template.connectionFactory = redisConnectionFactory()
        template.keySerializer = StringRedisSerializer()
        template.valueSerializer = GenericJackson2JsonRedisSerializer()
        return template
    }
}
yaml
# application-prod.yml
management:
  endpoints:
    web:
      exposure:
        include: "health,info,sessions"
      base-path: "/actuator"
  endpoint:
    sessions:
      enabled: true
  security:
    enabled: true

spring:
  session:
    store-type: redis
    timeout: 30m
    redis:
      flush-mode: on_save
      namespace: "myapp:session"
  
  redis:
    host: ${REDIS_HOST:localhost}
    port: ${REDIS_PORT:6379}
    password: ${REDIS_PASSWORD:}
    database: 0
    timeout: 2000ms
    lettuce:
      pool:
        max-active: 8
        max-wait: -1ms
        max-idle: 8
        min-idle: 0

logging:
  level:
    org.springframework.session: DEBUG

常见问题和解决方案 ❓

Q1: Sessions 端点返回空结果

WARNING

如果 /actuator/sessions?username=alice 返回空的 sessions 数组,可能的原因:

  1. 用户没有活跃会话 - 用户未登录或会话已过期
  2. Spring Session 未正确配置 - 检查 @EnableSpringHttpSession 注解
  3. 会话存储问题 - 检查 Redis 连接或内存存储配置
kotlin
// 调试代码:检查会话是否正确创建
@RestController
class SessionDebugController {
    
    @GetMapping("/debug/create-session")
    fun createTestSession(request: HttpServletRequest): Map<String, Any> {
        val session = request.getSession(true)
        session.setAttribute("username", "alice")
        session.setAttribute("testData", "debugging")
        
        return mapOf(
            "sessionId" to session.id,
            "creationTime" to session.creationTime,
            "maxInactiveInterval" to session.maxInactiveInterval,
            "attributes" to session.attributeNames.toList()
        )
    }
}

Q2: 删除会话后用户仍然可以访问

这可能是因为应用程序缓存了会话信息。解决方案:

kotlin
@Component
class SessionEventListener {
    
    @EventListener
    fun handleSessionDestroyed(event: SessionDestroyedEvent) {
        val sessionId = event.sessionId
        
        // 清理应用程序缓存
        clearUserCache(sessionId) 
        
        // 记录会话销毁日志
        println("会话已销毁: $sessionId")
    }
    
    private fun clearUserCache(sessionId: String) {
        // 实现缓存清理逻辑
        // 例如:从 Redis 缓存中移除用户信息
    }
}

总结 🎉

Spring Boot Actuator 的 Sessions 端点为我们提供了强大的会话管理能力:

可视化管理 - 让会话状态一目了然
安全控制 - 支持强制用户下线等安全操作
运维监控 - 帮助识别异常会话和安全威胁
灵活配置 - 支持多种存储方式和安全配置

通过合理使用 Sessions 端点,我们可以构建更加安全、可控的用户会话管理系统。记住在生产环境中一定要配置适当的安全措施,保护敏感的会话信息!

TIP

建议将 Sessions 端点与监控系统集成,设置会话数量告警,及时发现异常情况。同时,定期审查会话管理策略,确保符合安全最佳实践。