Skip to content

Spring WebFlux @CookieValue 注解详解 🍪

什么是 @CookieValue?

@CookieValue 是 Spring WebFlux 中的一个注解,用于将 HTTP 请求中的 Cookie 值绑定到控制器方法的参数上。它让我们能够轻松地从客户端发送的 Cookie 中提取特定的值,并在服务端进行处理。

NOTE

Cookie 是存储在客户端浏览器中的小型数据片段,通常用于会话管理、用户偏好设置、跟踪用户行为等场景。

为什么需要 @CookieValue?

传统方式的痛点

在没有 @CookieValue 注解之前,我们需要手动从 HTTP 请求中解析 Cookie:

kotlin
@GetMapping("/user-profile")
fun getUserProfile(request: ServerHttpRequest): Mono<String> {
    // 需要手动解析 Cookie
    val cookies = request.cookies
    val sessionCookie = cookies.getFirst("JSESSIONID") 
    
    if (sessionCookie != null) {
        val sessionId = sessionCookie.value
        // 使用 sessionId 处理业务逻辑
        return Mono.just("用户配置页面,会话ID: $sessionId")
    }
    
    return Mono.just("未找到会话信息") 
}
kotlin
@GetMapping("/user-profile")
fun getUserProfile(@CookieValue("JSESSIONID") sessionId: String): Mono<String> { 
    // 直接使用 sessionId,无需手动解析
    return Mono.just("用户配置页面,会话ID: $sessionId") 
}

@CookieValue 的价值

  1. 代码简洁性:一行注解替代多行解析代码
  2. 类型安全:自动类型转换,减少类型错误
  3. 可读性强:方法签名直观表达依赖的 Cookie
  4. 错误处理:内置的异常处理机制

基础用法示例

kotlin
@RestController
class UserController {
    
    @GetMapping("/welcome")
    fun welcome(@CookieValue("username") username: String): Mono<String> { 
        return Mono.just("欢迎回来,$username!")
    }
    
    @GetMapping("/session-info")
    fun getSessionInfo(@CookieValue("JSESSIONID") sessionId: String): Mono<Map<String, Any>> { 
        return Mono.just(mapOf(
            "sessionId" to sessionId,
            "status" to "active",
            "timestamp" to System.currentTimeMillis()
        ))
    }
}
kotlin
@RestController
class PreferenceController {
    
    @GetMapping("/theme")
    fun getTheme(
        @CookieValue(value = "theme", required = false) theme: String? 
    ): Mono<String> {
        val selectedTheme = theme ?: "default" // 如果 Cookie 不存在,使用默认值
        return Mono.just("当前主题: $selectedTheme")
    }
    
    @GetMapping("/language")
    fun getLanguage(
        @CookieValue(value = "lang", defaultValue = "zh-CN") language: String
    ): Mono<String> {
        return Mono.just("当前语言: $language")
    }
}

高级用法与类型转换

自动类型转换

Spring WebFlux 支持将 Cookie 值自动转换为不同的数据类型:

kotlin
@RestController
class AnalyticsController {
    
    @GetMapping("/visit-count")
    fun getVisitCount(
        @CookieValue("visitCount") count: Int
    ): Mono<String> {
        return Mono.just("您已访问 $count 次")
    }
    
    @GetMapping("/last-visit")
    fun getLastVisit(
        @CookieValue("lastVisit") timestamp: Long
    ): Mono<String> {
        val date = java.time.Instant.ofEpochMilli(timestamp)
        return Mono.just("上次访问时间: $date")
    }
    
    @GetMapping("/preferences")
    fun getPreferences(
        @CookieValue("notifications") notificationsEnabled: Boolean
    ): Mono<Map<String, Boolean>> {
        return Mono.just(mapOf("notifications" to notificationsEnabled))
    }
}

复杂对象转换

对于更复杂的数据结构,可以结合 JSON 序列化:

kotlin
data class UserPreferences(
    val theme: String,
    val language: String,
    val notifications: Boolean
)

@RestController
class AdvancedController {
    
    private val objectMapper = ObjectMapper()
    
    @GetMapping("/user-settings")
    fun getUserSettings(
        @CookieValue("userPrefs") prefsJson: String
    ): Mono<UserPreferences> {
        return try {
            val preferences = objectMapper.readValue(prefsJson, UserPreferences::class.java)
            Mono.just(preferences)
        } catch (e: Exception) {
            Mono.error(IllegalArgumentException("无效的用户偏好设置"))
        }
    }
}

实际业务场景应用

购物车功能

kotlin
@RestController
class ShoppingCartController {
    
    @GetMapping("/cart")
    fun getCart(
        @CookieValue(value = "cartId", required = false) cartId: String?
    ): Mono<ResponseEntity<Map<String, Any>>> {
        
        return if (cartId != null) {
            // 根据 cartId 获取购物车信息
            getCartItems(cartId)
                .map { items ->
                    ResponseEntity.ok(mapOf(
                        "cartId" to cartId,
                        "items" to items,
                        "total" to items.sumOf { it.price }
                    ))
                }
        } else {
            // 创建新的购物车
            createNewCart()
                .map { newCartId ->
                    ResponseEntity.ok()
                        .header("Set-Cookie", "cartId=$newCartId; Path=/; Max-Age=86400") 
                        .body(mapOf(
                            "cartId" to newCartId,
                            "items" to emptyList<Any>(),
                            "total" to 0
                        ))
                }
        }
    }
    
    private fun getCartItems(cartId: String): Mono<List<CartItem>> {
        // 模拟从数据库获取购物车商品
        return Mono.just(listOf(
            CartItem("商品1", 99.9),
            CartItem("商品2", 199.9)
        ))
    }
    
    private fun createNewCart(): Mono<String> {
        // 生成新的购物车ID
        return Mono.just(java.util.UUID.randomUUID().toString())
    }
}

data class CartItem(val name: String, val price: Double)

用户认证与会话管理

kotlin
@RestController
class AuthController {
    
    @GetMapping("/profile")
    fun getUserProfile(
        @CookieValue("authToken") token: String
    ): Mono<ResponseEntity<Map<String, Any>>> {
        
        return validateToken(token)
            .flatMap { userId ->
                getUserInfo(userId)
                    .map { user ->
                        ResponseEntity.ok(mapOf(
                            "user" to user,
                            "authenticated" to true
                        ))
                    }
            }
            .onErrorReturn(
                ResponseEntity.status(401)
                    .body(mapOf("error" to "无效的认证令牌"))
            )
    }
    
    @PostMapping("/logout")
    fun logout(
        @CookieValue(value = "authToken", required = false) token: String?
    ): Mono<ResponseEntity<String>> {
        
        return if (token != null) {
            invalidateToken(token)
                .then(Mono.just(
                    ResponseEntity.ok()
                        .header("Set-Cookie", "authToken=; Path=/; Max-Age=0") // 清除 Cookie
                        .body("退出登录成功")
                ))
        } else {
            Mono.just(ResponseEntity.ok("已退出"))
        }
    }
    
    private fun validateToken(token: String): Mono<String> {
        // 模拟令牌验证
        return if (token.startsWith("valid_")) {
            Mono.just(token.substring(6)) // 返回用户ID
        } else {
            Mono.error(IllegalArgumentException("无效令牌"))
        }
    }
    
    private fun getUserInfo(userId: String): Mono<Map<String, String>> {
        return Mono.just(mapOf(
            "id" to userId,
            "name" to "用户$userId",
            "email" to "$userId@example.com"
        ))
    }
    
    private fun invalidateToken(token: String): Mono<Void> {
        // 模拟令牌失效处理
        return Mono.empty()
    }
}

请求处理流程

让我们通过时序图了解 @CookieValue 的工作流程:

错误处理与最佳实践

异常处理

kotlin
@RestController
@ControllerAdvice
class CookieExceptionHandler {
    
    @GetMapping("/secure-area")
    fun secureArea(
        @CookieValue("authToken") token: String
    ): Mono<String> {
        return Mono.just("安全区域内容")
    }
    
    @ExceptionHandler(MissingRequestCookieException::class) 
    fun handleMissingCookie(ex: MissingRequestCookieException): Mono<ResponseEntity<String>> {
        return Mono.just(
            ResponseEntity.status(400)
                .body("缺少必需的Cookie: ${ex.cookieName}")
        )
    }
    
    @ExceptionHandler(TypeMismatchException::class) 
    fun handleTypeMismatch(ex: TypeMismatchException): Mono<ResponseEntity<String>> {
        return Mono.just(
            ResponseEntity.status(400)
                .body("Cookie值类型错误: ${ex.message}")
        )
    }
}

最佳实践

开发建议

  1. 使用有意义的 Cookie 名称:选择清晰、描述性的名称
  2. 合理设置 required 属性:根据业务需求决定是否必需
  3. 提供默认值:为可选的 Cookie 提供合理的默认值
  4. 注意安全性:敏感信息应该加密存储
  5. 设置合适的过期时间:避免 Cookie 无限期存在

WARNING

Cookie 存储在客户端,不要在 Cookie 中存储敏感信息(如密码、个人身份信息等)。对于敏感数据,应该只存储会话标识符,真实数据保存在服务端。

IMPORTANT

在生产环境中,务必为包含敏感信息的 Cookie 设置 HttpOnlySecure 标志,防止 XSS 攻击和中间人攻击。

总结

@CookieValue 注解是 Spring WebFlux 中处理 Cookie 的强大工具,它:

简化了代码:无需手动解析 HTTP 请求中的 Cookie
提供类型安全:自动进行类型转换
增强可读性:方法签名清晰表达依赖关系
支持灵活配置:可选参数、默认值等特性
适用多种场景:会话管理、用户偏好、购物车等

通过合理使用 @CookieValue,我们可以构建更加用户友好和功能丰富的 Web 应用程序! 🚀