一、问答问的是解析,工程上该问的是位置

有个问答描述的场景很常见:Cookie 里存了一个 JSON 字符串,前端想从中安全地提取单个变量,比如 token。这个问题本身不难答,难的是答完之后没什么用——它描述的是一个已经发生的设计后果,而不是一个需要技巧的操作。

先把操作层写对。如果这个 Cookie 已经存在、页面已经依赖它,正确的读取路径是这样:

  1. 从 document.cookie 里按名字定位那一条。同名 Cookie 可能因为 domain 或 path 不同而存在多份,取到的不一定是预期那份。
  2. 值先过 decodeURIComponent。写入时对 =、;、, 和中文做过编码的内容,不解码就直接 JSON.parse,会在第一个特殊字符上失败;浏览器自身对 Cookie 值的特殊字符也有处理,不能假设读回来的和写进去的完全一致。
  3. JSON.parse 包在 try/catch 里。”存的根本不是合法 JSON”不是异常场景,而是常态:拼接时漏了引号、内容中途被截断、被其他脚本改写过。catch 里要有明确的降级分支,不是把错误吞掉。
  4. 只把需要的字段取到局部变量。不要解析成对象后挂到全局,也不要让整个解析结果长期驻留在 window 上。
  5. 字段缺失与字段为空要分开处理。前者说明 Cookie 结构变了,后者说明会话本身已经没了,两者的后续动作不一样。

Cookie 中 JSON 令牌的解析与存放分层

这套写法的意义只是把损失控制在可预期范围内,它不会让这个设计变好。

二、”JSON 塞 Cookie”是三个问题叠在一起

第一个问题是体积。浏览器 Cookie 有约 4KB 的上限,而且这个预算是含名字与属性的,同一域名下所有 Cookie 共享。JWT 本身不短,再在 JSON 里附带一份用户信息,很快就把这点预算吃满;接近上限时超出的部分会被丢弃或截断,被截断的 JSON 就是下一轮解析异常的来源。

第二个问题是自动携带。Cookie 会随该域的每一个 HTTP 请求发送,包括图片、CSS、JS 这些静态资源。token 因此被反复传到并不需要它的地方,每一次都是无用字节加一次暴露面:服务端日志、CDN 日志、以及链路上任何记录请求头的组件,都会顺带存下它。

第三个问题是可读与可改。不设 HttpOnly 的 Cookie,前端脚本能读——能读就意味着任何一段被注入的脚本同样能读;能被脚本改写,就意味着把权限、角色这类字段放在里面,等于把授权决定交给浏览器。

三个问题叠在一起的结果是:一个既占了预算、又到处随包发送、还能被客户端篡改的载体,被用来存放整个系统里最敏感的那段字符串。

三、分层方案:按”谁读、谁带、谁能改”来放

正确的分层依据是谁需要读它,而不是哪个存储写起来方便。

  • 服务端会话标识:HttpOnly + Secure + SameSite 的 Cookie。浏览器脚本读不到,前端代码不需要知道内容,服务端凭它查会话。这一层放会话 ID,不放业务数据。
  • 短期 access token:需要出现在 Authorization 头里的,放内存变量,配合刷新机制;刷新令牌同样通过 HttpOnly Cookie 传递,前端不感知它的值。页面刷新会丢内存变量,这是这种设计的固有代价,靠静默续期流程解决,不是靠退回 localStorage 解决。
  • 业务字段(角色、权限、可见范围):不进前端可改的存储。前端需要渲染时向服务端要一份只读的展示数据,授权判定在服务端完成,前端拿到的只是一次请求的结果。
  • localStorage:XSS 可以直接读走,不该当令牌库。它适合放一旦丢失也不影响安全的东西,比如界面偏好。

判断表如下:

这段数据 谁读取 是否随每个请求发送 脚本能否修改 生命周期 应放位置
会话 ID 仅服务端 是(自动) 否(HttpOnly) 会话期 HttpOnly Cookie
短期 access token 前端代码显式携带 否(手动加头) 是(内存变量) 分钟级 内存 + 刷新机制
刷新令牌 仅服务端 是(限定路径) 否(HttpOnly) 较长 HttpOnly Cookie
角色 / 权限 服务端判定,前端展示 否 是 每次请求重算 不进前端存储
界面偏好 前端 否 是 长期 localStorage

四、存量改不动时,至少做三件收束

很多时候那段 JSON Cookie 属于存量系统,短期拆不动,也确实用不上完整分层。这种情况下至少做三件事:把 JSON 里非必要的字段删掉,只留最小标识;给这条 Cookie 加上 HttpOnly(如果服务端本来就在读它)或者明确它只由服务端写入;把前端解析代码里的失败分支变成显式的重新登录流程,而不是”取到 undefined 然后页面白屏”。这三条不改变架构,只是把故障模式从静默错误改成可解释的失败,为后续拆分留出时间。

有一条要单独说明:任何把权限字段放在客户端存储、由前端裁剪界面的做法,在服务端不做对应校验的前提下都不成立。这不是”暂时不够安全”,而是”根本没有做检查”——只要存在一个未鉴权的接口,问题就在那儿,跟数据放在哪个存储里无关。换成 HttpOnly Cookie 只是让客户端改不掉,替客户端做决定仍然是错的。

五、洹拓视角:评审时的四条检查项

代码评审或安全走查遇到与 Cookie、令牌相关的改动,用这四条判断。每条都要能给出可验证的答案,答不上就退回:

  1. 这条 Cookie 有多大,同域下所有 Cookie 加起来离上限还剩多少。让改动者给实测数字而不是估计,并且说明超预算时的处理方式(拆分、截断还是丢弃)在设计里写清了吗。
  2. 它会不会随静态资源请求发送。看这个域下静态资源的加载路径,如果 Cookie 会被带到图片或 JS 请求上,就要么收窄 Cookie 的 path 与 domain,要么把令牌从 Cookie 层挪走。
  3. 前端有没有能力修改它,修改后服务端是否照单全收。逐个检查读取这个字段的接口,看是否存在以客户端传来的角色、权限、金额为准的分支。这一条最容易判失败,而且与有没有加密无关。
  4. 解析失败的降级路径是什么。JSON.parse 抛异常之后用户去哪里,会话丢失是显式引导登录,还是留下一个半渲染的页面等用户报错。

这四条覆盖不了全部风险,但它们能回答真正该问的那个问题:这段数据现在是不是待在它该待的地方。