ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

子cookie深度解析:原理、常见误区与实战用法

子cookie深度解析:原理、常见误区与实战用法 1. 子cookie到底是什么名字唬人本质只是编码约定1.1 从cookie的家底说起一个cookie到底能装多少东西聊子cookie之前得先把cookie本身的规则捋清楚。很多人天天在用cookie但真被问到一个cookie最大能存多少、一个域名最多能种几个时能答上来的人不多。浏览器这边的通行标准是RFC 6265它规定每个域名下最少要有50个cookie的配额每个cookie本体含key、等号、value最少要有4096字节的容量。注意这里有个坑4096字节算的是整个cookie串不是value部分很多人把4KB全算在value头上结果存数据存到超限怎么被截断的都不知道。那子cookie是什么一句话就能说明白子cookie并不是浏览器规范里定义的一种特殊cookie而是开发者自己约定的一种数据编码方式把多个名值对塞进同一个cookie的value里形如a1b2c3。它没有一个专属的浏览器API也没有独立的属性字段本质上就是在cookie这一个壳子里用约定的分隔符放下多个小数据项。这套做法和JSON塞进localStorage是一个思路区别在于cookie会自动随HTTP请求头往返localStorage不会。换句话说子cookie的独特价值不是多存点而是在不多占cookie数量的前提下让多个数据项一起自动发给服务端。1.2 子cookie的真实形态一个key里塞了一组名值对直接看一段实际存在的cookie长什么样。假设一个站点要在客户端记录用户偏好、会话标识和一次A/B实验的分组按常规思路可能会种三个cookieSet-Cookie: user_langzh-CN; Path/; Max-Age2592000 Set-Cookie: session_idabc123; Path/; HttpOnly Set-Cookie: exp_groupB; Path/; Max-Age2592000用子cookie的思路这三个就合并成一个Set-Cookie: prefslangzh-CNsidabc123expB; Path/; Max-Age2592000看到区别了吗prefs是cookie名后面那段langzh-CNsidabc123expB就是子cookie串。整个cookie在浏览器开发者工具里显示为一条记录但value里实际装了三组键值。需要注意这种编码方式完全是约定出来的没有任何规范强制你用什么分隔符。有人用有人用|有人用~有人甚至用JSON序列化后整体塞进去。分隔符选什么、键怎么命名、值要不要编码全靠团队自己定。这一灵活性带来的副作用就是接收方必须严格遵循同样的解析规则前后端一旦有一方理解错了轻则读不到数据重则把整条cookie解析崩掉。1.3 这套做法的历史成因被20个cookie限制逼出来的土办法子cookie之所以在早年特别流行和旧浏览器的存储限制有直接关系。2000年前后的RFC 2109时代规范要求浏览器支持每域名至少20个cookie比现在的50个少得多而IE6、IE7这些当年的主流浏览器实际表现得更保守。当时一个稍复杂的站点光埋点、会话、用户偏好、AB实验就能轻松消耗十几个cookie一旦配额用完新cookie要么被静默丢弃要么把最老的cookie挤掉线上问题非常诡异。在那种环境下把多个数据项打包进一个cookie就成了一种刚需数量上只占一个名额通过或者|把数据摊薄在value里。虽然后来浏览器普遍放宽到了50个乃至更多Cookie总大小也稳定在4KB左右但很多老项目里的子cookie代码一代传一代一直传到了今天。因此你现在看到的很多子cookie封装工具其实是历史的遗产谈不上先进但在特定场景下依然有用。2. 最容易踩的四个误区把子cookie当独立cookie用就完了2.1 误区一给某个子cookie设过期时间指望它单独失效这是我见过最多、踩的人死得最惨的一个坑。先看一段典型的错误写法// 错误示范试图给子项设置过期时间 document.cookie prefslangzh-CN; max-age0;写下这行代码的人脑子里想的是我把prefs里的lang这一项删掉但浏览器根本不管你的子项是什么它只认prefs这个整体cookie。上面的操作实际效果是把整个prefs直接删掉了session_id、exp这些子项全部跟着消失。这里的根本原因在于过期时间是一个cookie级别的属性。你可以给整个prefs设置max-age或expires但你没有任何机制对prefs内部的lang单独设置生命周期。当你需要让某个子项提前失效时唯一靠谱的方式是重写整个cookie把不需要的子项剔除后重新生成完整的value再写回去。网上有些教程会变着花样说把子项的过期时间设为过去以单独删除这本质上是在偷换概念落地时还是要回到整包重写。不要被字面说法骗了。2.2 误区二删一个子项以为重写整个cookie就行——结果全没了有人会说我知道要重写那我直接重写整个cookie不就行了逻辑上没错但操作里处处是雷。看这段代码// 脑子里想做的是从 prefs 里删除 lang保留 sid 和 exp document.cookie prefssidabc123expB; Path/; Max-Age2592000;表面看上去确实把lang去掉了只写了sid和exp。但问题出在很多人写重写代码时容易丢掉属性。重写cookie时如果新的cookie没有带上Path很可能和旧cookie的Path不一致导致浏览器里同时存在两条prefs——一条Path/一条Path/some/path。前者能看到后者看不到排查起来一头雾水。更隐蔽的坑是你不知道当前cookie里到底有哪些子项。比如这段prefs可能由后端Java写入、前端JS写入、另一个团队的中台系统也写入你重写时只按自己知道的字段来拼其他字段就被你悄悄弄丢了。所以删除单个子项之前正确流程是先把整个cookie读回来解析成对象删除目标键再把剩下的键重新序列化后写回。整个链路缺一步都是事故。2.3 误区三觉得HttpOnly、Secure这些属性是按子项生效的这条误解在面试里出现频率很高实际项目里也很致命。有人会写出类似这样的方案构思我给session_id这个子项打上HttpOnly防止XSS读取给lang这个子项不打HttpOnly让JS能改。这个想法在概念上就错了。HttpOnly、Secure、SameSite这些属性全部是cookie级别的作用于整个cookie不存在某个子项可以单独设置HttpOnly这回事。你只要给prefs打上HttpOnly那整条prefs对document.cookie就不可见了JS既读不到也写不了。反过来如果你为了能让JS更新某个子项而放弃HttpOnly那么整个cookie包括session_id都暴露在XSS风险之下。这事儿的本质是子cookie只是把若干数据项物理打包了并没有把它们的安全等级也分开。如果你需要不同等级的安全边界就应该用不同cookie去承载而不是塞进同一个子cookie串里。这一条想不通后面所有的封装都容易出安全问题。2.4 误区四不同后端/框架对子cookie的处理方式不一样在纯前端视角里子cookie就是读一读、写一写似乎很自由。但一旦有后端参与问题就来了。后端框架里对cookie的读取通常是直接按cookie名取值的比如Java的request.getCookies()遍历后按name匹配拿到prefs的value后自己再split——这个split的逻辑后端写错了前端传再标准也没用。更麻烦的是框架本身的序列化策略。有些老版本的框架会在cookie value上做URL编码把编码成%26把编码成%3D。如果你前端用拼接子cookie后端拿到的value里根本没有全是%26解析逻辑没做decode的话整个子cookie就废了。另外很多后端框架默认会给cookie加引号包裹value子cookie串会被双引号包起来解析时忘了去引号也会出事故。所以前后端关于子cookie的格式约定必须成文至少要包含四件事分隔符用什么、键名大小写是否敏感、value是否需要URL编码、遇到空值怎么处理。这四件事没对齐线上数据就会歪。3. 什么时候才值得用子cookie算清楚效益再动手3.1 使用价值最大的场景降低请求头体积把多个cookie合并成一个最大的收益不是省cookie名额而是压缩HTTP请求头的体积。很多人没有算过这笔账cookie的附加成本其实很惊人。一个普通cookie在请求头里至少包含Cookie: namevalue这段连同分号和空格一个5字节的value实际要占15字节左右。如果你的站点种了10个像session_idabc123这样的小cookie请求头里的cookie段大约150字节。看起来不多但每个请求都要带一个页面可能有几十个请求累积起来就是几十KB的重复流量。如果是移动端弱网环境每个请求都要为这些重复数据等一个RTT时间体感差别很大。用子cookie把10个cookie合并成1个请求头里就只剩一段value可能稍长但总字节数通常能压缩50%左右因为省掉了大量键名和多余的分隔符。我做过一个实际案例把某个页面首屏请求的cookie头从870字节压到410字节差不多省了一半。这种优化在HTTP/1.1时代效果显著在HTTP/2时代因为头部压缩的存在收益会打折扣但依旧可观。3.2 别硬上子cookie的场景数据本身就该用Web Storage子cookie不是万能的很多场景用它属于自找麻烦。比如说你有一份用户偏好设置只在浏览器端使用压根不需要发给服务端。这种情况下用localStorage存多少都没关系5MB配额摆在那里读写也只是同步操作简单直接。非要用子cookie的话数据还要塞在4KB的cookie里挤空间每次请求还得白白背上这份流量纯粹是浪费。再比如你的数据项之间生命周期差异特别大会话token几分钟就过期用户偏好可以放一年。这种生命周期天然分群的数据硬塞进一个子cookie里要么因为token的更新导致整个cookie频繁重写要么让偏好数据跟着短命token一起消亡两边都别扭。还有跨标签页实时同步的需求。localStorage有storage事件可以监听cookie也有自己的轮询和监听方案但子cookie的自定义格式让同步逻辑复杂不少收益却很低。这种场景直接用标准方案更香。3.3 一次真实的存储规划把cookie配额算给你看我给你一个实际可参考的规划例子。假设一个B端系统需要在前端记录以下数据数据项内容示例预估字节是否必须传给后端会话令牌tokeneyJhbGciOiJIUzI1NiJ9...180是用户语言langzh-CN6是当前租户IDtenant102412是列表每页条数pageSize208否侧边栏折叠状态sidebar16否最近搜索关键词recentvue312否按常规思路前三个用普通cookie后三个用localStorage这样分工最合理。但如果团队规定所有后端需要的数据必须放cookie里且每个域名下cookie数量不超过10个那前三个又该如何取舍三个普通cookie每个都带Path/; SameSiteLax这些固定成本单条cookie的传输开销已经是value本身的数倍。改成一条子cookie后整个会话相关的信息打包进authtokenxxxlangzh-CNtenant1024请求头里就一段后端解析一次就能拿到全部字段既减少了cookie条数又压缩了header体积。这才是子cookie该出现的舞台。4. 正确用法序列化、解析与更新的落地姿势4.1 序列化分隔符、编码规则和长度预算写子cookie的第一步是定协议也就是value内部的格式。没有标准可循但我强烈建议你至少遵守三条原则第一值必须编码。因为子cookie值里可能包含、、;、空格、中文等特殊字符直接拼接会把解析器搞崩溃。统一用encodeURIComponent编码解析时用decodeURIComponent还原。第二分隔符要避开所有可能出现在value里的字符。和是URL查询串里的老搭档语义清晰、不易混淆建议优先用它们做主分隔符和键值分隔符。不要用逗号或分号cookie规范里分号是属性分隔符逗号在某些框架里会被特殊处理。第三明确Size预算。浏览器4KB限制指的是整个cookie也就是namevalue加上所有属性。所以子cookie的value部分实际可用空间大概在3.5KB左右。写代码前先算一下最坏情况下所有子项的总长度超了就拆成两条cookie别把宝全押在一个鸡蛋里。序列化的工具函数很简单function serializeSubcookie(obj) { return Object.entries(obj) .map(([key, value]) ${encodeURIComponent(key)}${encodeURIComponent(String(value))}) .join(); }4.2 解析别信直觉写字解析器要处理脏数据解析子cookie最容易犯的错是假设数据永远干净。实际上cookie数据脏得很可能是旧版本格式可能被用户手动改过可能被浏览器插件截断过可能因为超长被静默砍掉后半截。写着宽容的解析器才能扛住线上真实环境。function parseSubcookie(str) { if (!str) return {}; const result {}; str.split().forEach((pair) { if (!pair) return; const eqIndex pair.indexOf(); if (eqIndex -1) { // 没有等号按空值处理 result[decodeURIComponent(pair)] ; return; } const key decodeURIComponent(pair.slice(0, eqIndex)); const value decodeURIComponent(pair.slice(eqIndex 1)); result[key] value; }); return result; }注意这里有几个细节。一是用indexOf()而不是split()防止value里残留未编码的等号导致数组长度大于2。二是遇到没有等号的裸键不直接丢弃按空值解析保证向后兼容。三是decodeURIComponent要包在try/catch里防止用户手动改坏了cookie导致整页JS报错。读cookie值时规则是在document.cookie里按name定位而不是简单地split所有分号。因为分号也可能是value里的编码内容虽然encodeURIComponent会把分号编码成%3B但用户手动改的脏数据可不管这一套。4.3 更新与删除整包重写缺一不可更新子cookie的正确姿势是读-改-写三步走每一步都要稳。function updateSubcookie(name, key, value) { const raw getCookie(name); // 自己写一个按名取值的函数 const obj parseSubcookie(raw); obj[key] value; setCookie(name, serializeSubcookie(obj), { path: /, maxAge: 2592000 }); }删除某个子项同样遵循这个模式区别只在最后一步是删除对象属性function removeSubkey(name, key) { const obj parseSubcookie(getCookie(name)); delete obj[key]; setCookie(name, serializeSubcookie(obj), { path: /, maxAge: 2592000 }); }写回的时候有几个坑必须提。第一属性要和原cookie完全一致尤其是Path不一致会导致读写不同步。第二要显式带上过期时间不让浏览器把它当成会话cookie。第三最好在写完后立即读回来做一次断言校验防止写入失败后代码无感知后续逻辑读到旧值继续跑。5. 和现代方案放一起看子cookie的适用边界5.1 一张表看清四类本地存储前端本地存储方案现在远不止cookie一种把它们放在一起比较边界就清楚了。方案容量是否随请求发送生命周期跨标签页通信安全性cookie4KB/个50个/域是可设过期时间无原生事件HttpOnly可防XSS读取localStorage5MB左右否持久直到手动清除storage事件XSS可读无法HttpOnlysessionStorage5MB左右否标签页关闭即失同标签页内XSS可读IndexedDB通常不限否持久无原生事件XSS可读异步复杂cookie在是否随请求发送这一栏是唯一这是它的立身之本。子cookie则在不脱离cookie的前提下优化了它的存储效率和条数管理。而localStorage和IndexedDB虽然在容量上碾压cookie但在自动携带这件事上完全替代不了。5.2 哪些项目我还在用子cookie哪些坚决不用我对子cookie的使用态度可以概括成一句话会话相关的轻量元数据适合用业务数据坚决不用。具体来说我目前还在用子cookie的场景有两类。一类是网关鉴权信息多个微服务共享的token、tenant、userId、语言标识打包在一个cookie里后端网关解析一次就能拿到全部上下文省去了多个cookie的重复解析请求头也清爽。另一类是A/B实验分组页面里可能同时跑着五六个实验每个实验一个分组编号如果各自独立成cookie一下就占掉六七个名额用子cookie打包成一个expexp1Aexp2Bexp3C管理起来方便得多。坚决不用的场景是图片验证码的校验值、表单草稿、用户偏好这种本就不该出浏览器的数据。这些数据塞进cookie里不但让每次请求的header变大还增加被攻击面收益为零纯属自残。我甚至见过有人把购物车完整JSON塞进cookie的一个购物车几十KB直接就把cookie撑爆了页面功能就乱了。5.3 如果决定要用这几条铁律收好根据我踩过的坑给你几条实战铁律。第一格式文档化。用子cookie之前先写一份简短的接口文档包含分隔符、编码规则、字段含义和示例贴在项目README里。这份文档能救你一命尤其在团队换人之后。第二总大小留余量。不要把一个cookie的value用到3.5KB以上留出至少500字节的余量因为后续加字段很快。一旦触发4KB截断数据悄悄丢一片排查起来极其痛苦。第三解析函数要容错。上面给的parse函数里加上try/catch解析失败时返回空对象而不是让异常冒泡打断页面主流程。cookie不是可信数据源它可能被用户、插件、代理任意改写防御性编程是底线。第四服务端解析逻辑同步维护。如果cookie要被后端使用确保后端解析逻辑和前端一致。最好前后端共用一份协议说明并用同一组测试用例覆盖避免两边个自发挥。第五尽量用成熟的第三方库。如果项目不太讲究自研可以用js-cookie这类库来做cookie的基础读写再在其上封装子cookie逻辑比自己手撸document.cookie靠谱得多。基础能力交给经过验证的库业务封装留在自己手里责任边界清晰。5.4 最后的小提醒说道底子cookie是个看起来简单、用起来坑多的方案。它没有标准实现完全靠约定适合处理那些必须放在cookie里、又不想占太多名额的轻量数据。决定用它之前先问自己三个问题这些数据必须跟请求走吗合并成一个cookie后我对它们的管理逻辑还清晰吗前后端解析约定能保持一致吗三个问题都回答是再用不迟。至于面试里被问到子cookie有什么用我的建议是别去背那些八股答案把这里的实际场景讲清楚比空谈概念有用得多。存储这事儿从来不是选最先进的方案而是选最合适当下约束的方案。子cookie至今没消失就是因为它在一个特定角落里确实还有别的方案暂时替代不了的位置。
返回列表