Appearance
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 的价值
- 代码简洁性:一行注解替代多行解析代码
- 类型安全:自动类型转换,减少类型错误
- 可读性强:方法签名直观表达依赖的 Cookie
- 错误处理:内置的异常处理机制
基础用法示例
简单的 Cookie 值获取
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()
))
}
}
可选的 Cookie 值
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}")
)
}
}
最佳实践
开发建议
- 使用有意义的 Cookie 名称:选择清晰、描述性的名称
- 合理设置 required 属性:根据业务需求决定是否必需
- 提供默认值:为可选的 Cookie 提供合理的默认值
- 注意安全性:敏感信息应该加密存储
- 设置合适的过期时间:避免 Cookie 无限期存在
WARNING
Cookie 存储在客户端,不要在 Cookie 中存储敏感信息(如密码、个人身份信息等)。对于敏感数据,应该只存储会话标识符,真实数据保存在服务端。
IMPORTANT
在生产环境中,务必为包含敏感信息的 Cookie 设置 HttpOnly
和 Secure
标志,防止 XSS 攻击和中间人攻击。
总结
@CookieValue
注解是 Spring WebFlux 中处理 Cookie 的强大工具,它:
✅ 简化了代码:无需手动解析 HTTP 请求中的 Cookie
✅ 提供类型安全:自动进行类型转换
✅ 增强可读性:方法签名清晰表达依赖关系
✅ 支持灵活配置:可选参数、默认值等特性
✅ 适用多种场景:会话管理、用户偏好、购物车等
通过合理使用 @CookieValue
,我们可以构建更加用户友好和功能丰富的 Web 应用程序! 🚀