spring 的 @CookieValue 注解 用法

@CookieValue 是 Spring MVC 中用于访问 HTTP Cookie 的核心注解,它提供了一种简洁高效的方式将 Cookie 值绑定到控制器方法参数上。

核心属性详解
属性 类型 默认值 说明
value/name String "" Cookie 名称
required boolean true 是否必须存在
defaultValue String ValueConstants.DEFAULT_NONE 默认值(当 Cookie 不存在时使用)
  • value / name :指定要读取的Cookie名称,两者是互为别名的关系,用哪个都行。
  • required :默认是 true ,表示如果取不到这个Cookie,框架直接抛异常,最终表现是接口返回400错误。
  • defaultValue :默认值是 ValueConstants.DEFAULT_NONE ,一个内部约定字符串,用来区分"用户没有设置默认值"和"用户设置的默认值是空字符串"这两种情况。

使用demo:

java 复制代码
@GetMapping("/dashboard")
public String dashboard(@CookieValue("SESSION_ID") String sessionId) {
    // 使用sessionId获取用户信息
    User user = sessionService.getUser(sessionId);
    return "dashboard";
}
java 复制代码
@GetMapping("/preferences")
public String preferences(@CookieValue(value = "THEME", defaultValue = "light") String theme) {
    // 如果Cookie不存在,使用默认值"light"
    return "preferences-" + theme;
}

使用案例

一、用Cookie给用户打组

AB实验是 @CookieValue 非常典型的应用场景。运营同学做新功能灰度时,往往希望一批用户看到新版界面,另一批用户看到旧版界面,并保证同一个用户多次访问始终落在同一分组里。通常的做法是用一个独立的Cookie记录用户分组,比如 AB_TEST_GROUP=experiment 。

java 复制代码
@GetMapping("/recommend")
public RecommendResult recommend(
        @CookieValue(name = "AB_TEST_GROUP", defaultValue = "control") String group) {
    boolean isExperiment = "experiment".equals(group);
    return recommendService.getRecommend(isExperiment);
}

这个接口在 group 为 experiment 时走新版推荐策略,其它情况走老版策略。由于设置了 defaultValue = "control" ,新用户第一次没被种Cookie时也不会报错,框架自动兜底到对照组。

这一类需求最容易被忽略的细节是: AB_TEST_GROUP 这个Cookie的 Path 和 Domain 一定要和页面请求路径保持一致,否则会出现用户明明被种了实验分组,接口却拿不到值的现象。

二、用户维度限流:自定义注解+AOP+@CookieValue

先定义一个限流注解:

java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserRateLimit {
    int qps() default 10;
    int windowSeconds() default 60;
}

再写一个AOP切面,从Cookie中取用户身份:

java 复制代码
@Aspect
@Component
public class UserRateLimitAspect {

    @Around("@annotation(userRateLimit)")
    public Object limit(ProceedingJoinPoint joinPoint, UserRateLimit userRateLimit) throws Throwable {
        ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attrs.getRequest();
        String userId = resolveUserIdFromCookie(request);
        if (userId == null) {
            throw new UnauthorizedException("未登录");
        }
        String key = "rate:user:" + userId;
        long count = redisTemplate.opsForValue().increment(key);
        if (count == 1) {
            redisTemplate.expire(key, Duration.ofSeconds(userRateLimit.windowSeconds()));
        }
        if (count > userRateLimit.qps()) {
            throw new RateLimitException("请求过于频繁");
        }
        return joinPoint.proceed();
    }

    private String resolveUserIdFromCookie(HttpServletRequest request) {
        Cookie[] cookies = request.getCookies();
        if (cookies == null) {
            return null;
        }
        for (Cookie cookie : cookies) {
            if ("USER_ID".equals(cookie.getName())) {
                return cookie.getValue();
            }
        }
        return null;
    }
}

然后在接口上直接用注解:

java 复制代码
@GetMapping("/submit")
@UserRateLimit(qps = 5, windowSeconds = 60)
public Result submit() {
    // 业务逻辑
}

这个组合拳的价值在于:Controller方法里不需要出现任何Cookie相关代码,限流逻辑完全沉淀在切面层,业务代码保持干净。如果你对"根据Cookie识别用户"和"基于注解做接口限流"这两个点都感兴趣,非常推荐把上面这套组合方式应用到项目里,实测下来比在拦截器里写一段模板代码可维护得多。

三、多语言与主题偏好:一个注解完成偏好读取

用户偏好类Cookie也非常适合 @CookieValue 。比如站点支持多语言,用户切换语言时前端写一个 LANG=zh-CN 的Cookie,后端接口读取这个值动态组装文案或数据。

java 复制代码
@GetMapping("/article")
public ArticleVO getArticle(
        @CookieValue(name = "LANG", defaultValue = "zh-CN") String lang,
        @CookieValue(name = "THEME", defaultValue = "light") String theme) {
    ArticleVO article = articleService.findById(articleId);
    article.setTheme(theme);
    article.setContent(i18nService.localize(article.getContent(), lang));
    return article;
}

这里两个 @CookieValue 共用同一个请求,框架会分别独立解析两个不同名称的Cookie,互不干扰。即使浏览器只带了其中一个,另一个也会被 defaultValue 兜住,不会因为缺少一个偏好项导致整个接口报400。

高频问题速查表
现象 可能原因 解决办法
接口返回400 Bad Request required=true 但请求未携带Cookie 设置 required=false 或补充 defaultValue
拿到null但浏览器明明有Cookie Cookie名不匹配;依赖了参数名推断;Path范围不对 显式指定 name / value ,检查Cookie作用域

|------------------|---------------------------------------|--------------------------------------------------|
| Cookie值中文或特殊字符乱码 | Cookie值不满足RFC 6265,未做编码 | 写入时 URLEncoder.encode ,读取时 URLDecoder.decode |
| 前端JS拿不到Cookie | Cookie设置了 HttpOnly | 服务端读取不受影响,前端通过接口获取 |
| 跨域请求带不上Cookie | SameSite 限制或跨域未配置 withCredentials | 合理设置 SameSite ,前端请求开启凭证携带 |

四个让我记忆深刻的教训

第一个教训:在旧接口上新增 @CookieValue 参数,没有设置 required=false ,导致线上大量老客户端请求直接400。老客户端不会主动携带新Cookie,而新参数又强制必填,这一改直接把兼容性改没了。从那以后,凡是给已有接口加Cookie参数,我一律先把 required 设为 false 或者给默认值,经过版本迭代确认客户端都改造完成后再收紧。

第二个教训: @CookieValue String user 依赖参数名推断,结果项目编译参数没保留方法名,Spring里拿到的参数名变成 arg0 ,线上一直查不到这个Cookie,排查了一下午才发现是对不上参数名。这个坑在上面2.3节已经详细说过,这里再强调一次:显示写 name ,杜绝一切隐性依赖。

第三个教训:Cookie值里放了签名信息,但签名结果中包含 + 、 = 、 / 等字符,写入和读取时没有做URL编码,导致服务端拿到的值和生成签名时的原文不一致,验签永远失败。这个问题的本质是Cookie值和URL传输之间的编码约定不一致,解决方式是在服务端写入前统一编码、读取后统一解码。

第四个教训:有人图省事在Controller里直接用 @CookieValue 取用户角色做权限判断,绕过了拦截器的统一鉴权,结果Cookie被篡改后越权访问了管理接口。Cookie本来就是客户端可控数据,不能作为安全边界,权限判断必须依赖服务端签发的Token和统一鉴权组件, @CookieValue 只负责把值取出来,不负责验证值的真伪。

相关推荐
天远Date Lab1 小时前
零信任架构实战:基于天远企业股权变更构建自动化招投标供应商股东穿透审查网关
java·人工智能·架构·自动化
god_cvz1 小时前
麒麟 V10(ARM64 / aarch64)源码编译 3dtiles 全步骤
后端·gis
武子康1 小时前
批量设备任务显示 COMPLETED,为什么还有设备失败?
人工智能·后端·agent
weixin_399380692 小时前
TongWeb7.0.4.9M11如何接入到idea(by lqw)
java·ide·intellij-idea
paopaokaka_luck2 小时前
社区服务平台(加权协同过滤推荐、Jieba中文词频分析、WebSocket实时聊天、求助处理闭环、 活动积分闭环、ECharts多维分析)
java·前端·spring boot·mysql·echarts·mybatis
泡海椒2 小时前
折线图Y轴刻度悬空之谜-信用评级趋势
后端
.Bryce.2 小时前
T2-LLM评测不等于Agent评测
android·java·人工智能·后端·gpt·agent·1024程序员节
Wang's Blog2 小时前
Java 项目实战: 交友项目 - 用户体系集成:查询用户环信账号密码
java·交友
Wang's Blog2 小时前
Java 项目实战: 交友项目 - 发布评论
java·开发语言·交友