神奇的网页记事本(一)— 浏览器存储技术之Cookie

前言

浏览器本身常见的存储技术有:Cookie、localStorage、sessionStorage、IndexedDB、Web SQL Database、Cache Storage 等。

  1. Cookie :用于维持请求状态的小型文本文件,由网站通过用户的浏览器创建并存储。
  2. localStorage:localStorage 是 Web Storage API 的一部分,它是一种在浏览器中存储持久化数据的机制。它允许网页在用户的浏览器中保存键值对数据,并且这些数据在浏览器关闭后仍然有效,没有过期时间。
  3. sessionStorage:sessionStorage 也是 Web Storage API 的一部分,它是一种在浏览器中存储临时会话数据的机制。与 localStorage 不同,sessionStorage 中存储的数据仅在浏览器会话期间有效,即在用户关闭浏览器选项卡或窗口后,这些数据将被清除。
  4. IndexedDB: IndexedDB 是浏览器上的索引数据库,它允许网页在用户的浏览器上进行复杂的查询和事务操作。
  5. Web SQL Database: Web SQL Database 是一个基于 SQL 的浏览器数据库,可以在客户端存储结构化数据。虽然它曾经是 HTML5 的一部分,但目前已经被弃用,不再被推荐使用。现代浏览器更倾向于支持 IndexedDB
  6. Cache Storage: Cache Storage 是 Service Worker 的一部分,允许开发者对网络请求和响应进行缓存。它可以用于在离线状态下提供快速的资源访问,以及优化网页的性能和加载速度。

除了上面提到的存储技术,还有一些不常用的存储技术,我们打开控制台的 Application 就可以看到一系列和存储相关的选项:

本篇主要是讨论前面提到的第一种浏览器存储技术: Cookie

Cookie 的作用不是为了存储数据,而是为了 维持状态 。我们都知道 http协议是无状态的 ,这就会导致每一次请求都是相互独立,不方便服务器识别用户身份和追踪用户行为,因此就诞生了 Cookie 技术,服务器可以设置和读取 Cookie 信息,以实现 会话跟踪 和 身份认证 等功能。一般来说 Cookie 是由服务端生成,由客户端进行存储和维护的,浏览器在发起 http 请求时,携带 Cookie 信息发送给服务器,由服务器辨别用户身份。

为了保护用户隐私,大多数主流浏览器都禁止 JavaScript 修改 Cookie 请求头。只有当请求与当前页面的源(协议、域名和端口)相同时,浏览器才会默认自动添加 Cookie 请求头并携带相应的 Cookie 信息。如果通过 js 设置 Cookie 请求头就会发生错误:Refused to set unsafe header "cookie"

  1. 服务端生成。服务端根据 http 响应头的 set-cookie 设置 Cookie。
js 复制代码
Set-Cookie: uid=fb3eda1aa35a9ed9f88f346a7; Path=/
  1. 客户端生成。客户端的 js 通过 document.cookie 也可以读写 Cookie。
js 复制代码
document.cookie="name=张三";
document.cookie="age=18; domain=juejin.im; path=/";
  1. 大小限制: 单个 Cookie 的大小通常限制在 4KB 左右,所以如果一个网站设置了的 Cookie 太大,可能会导致浏览器拒绝保存新的 Cookie。
  2. 数量限制 : 每个域名下的 Cookie 的个数是有上限,不同浏览器的上限不一样。如果一个网站设置了太多的Cookie,可能会导致浏览器覆盖旧的 Cookie。对于 Chrome(92) 浏览器每个 Cookie 字符串最长是 1024 个字符串(document.cookie 后面的字符串长度,包括 path、domain等,如果只有 name=value, 则 name=value 这个字符串长度最长为1024),超长的字符串会导致设置 cookie 失败。对于 Chrome 的同域名下的 cookie 个数最多为 176 个左右,不同版本可能都不太一样。
  3. 跨域限制:浏览器通常会限制来自其他域名的 Cookie 访问。当一个网页发起跨域请求时(例如使用XMLHttpRequest、Fetch API等),浏览器默认情况下不会在请求中发送跨域域名下的 Cookie 信息,即使在请求时手动增加请求头 Cookie 也会被浏览器过滤掉。再即使服务器在响应请求时设置了 Cookie,浏览器也不会将该 Cookie 存储起来,也不会在后续的跨域请求中携带这个 Cookie。

跨站和跨域两者不是同一个概念。不同站的cookie都是第三方cookie,不跨站的cookie成为第一方cookie。关于跨站的说明可以参考下面👇🏻引用:

以下内容来源于:深入理解 Cookie 的 SameSite 属性--小小木锤

所谓的 跨站 和 跨域 其实不是同一个概念,跨站 不是根据同源策略(协议,主机,端口)来判断,而是 PSL(公共后缀列表)。比如 foo.example.com 和 bar.example.com 就不属于 跨站 ,因为他们同属于 example.com 的子域名。这里也不能简单理解为二级域名相同,比如 foo.github.io 和 bar.github.io,虽然都是 github.io 的子域名,但是他们之间是跨站 的,因为 github.io 是在 PSL(公共后缀列表) 中的,相当于顶级域名,可以在 此处 查看哪些域名是属于 PSL 的。还有就是协议和域名相同时,如果端口不同,两个页面也会认为是同站 的;但是如果域名和端口相同,协议不同,这时候就认为这两个页面是跨站的

要实现 Cookie 的跨站访问需要前后端协调开发,下面是跨站请求读写 Cookie 的前提(必需全部满足):

  1. 前端配置 withCredentials(允许请求时携带凭据)。
    • 原生 XMLHttpRequest : (new XMLHttpRequest()).withCredentials = true;。
    • axios : axios.defaults.withCredentials = true;。
    • fetch请求 :fetch(url, { credentials: true })。
  2. 前端页面的域名在后端允许跨域的白名单中(Access-Control-Allow-Origin 中包括前端页面所在域名)。
  3. 后端也需要允许请求时携带凭据(响应头:Access-Control-Allow-Credentials:true)。
  4. 在新版(v80+)的 Chrome 浏览器中,只有指定 Cookie 的 SameSite 属性为 None 且 Secure 属性为 true 才可以设置第三方 Cookie(因为chrome80+中,samesite默认值是lax);如果域名相同,但是端口或者协议不同 samesite 的值也可以是lax。

从下图可以看出 Cookie 是以键值对的形式存在的,每个 Cookie 的信息包括 Name、Value、Domain、Path、Expires/Max-age、Size、HttpOnly、Secure、SameSite、Party Key、Priority 字段,各个键名都不区分大小写。

Name & Value

Name

Cookie 的 Name(键名) 是大小写敏感的,即 age 和 Age 是两个不一样的键名。在同一个页面下,可以存在多个同名的 Cookie,前提是它们的 Domain 或者 Path不同;如果 Cookie 的 Name、Domain、Path 三者都一致的话,后面设置的 Cookie 会覆盖前面设置的。

关于命名上的要求:

  • 必须是字符串(可以为空,不建议为空,因为不同浏览器、服务器支持性不一致)
  • 不包括控制字符(CTL)的 US-ASCII 字符串
  • 不能含有 ;、=(应该还有其他的特殊字符没有列出)

建议键名是只包含英文字母(a-zA-Z)、数字(0-9)、下划线(_)和中横线(-)的语义明确的字符串。如果键名设置不规范的话,可能会导致 Cookie 设置不成功或者乱码现象(我在chrome中可以将name设置为中文,但是作为请求头发送给服务器就会乱码)。

Value

Cookie 的值,规范的取值可以参考RFC6265文档:

"cookie-octet" 是指允许在Cookie值中使用的ASCII字符,它包括:
%x21 :ASCII值为33的字符,即叹号 !
%x23-2B :ASCII值从35到43的字符,即 # $ % & ' ( ) * +
%x2D-3A :ASCII值从45到58的字符,即 - . / 0-9 :
%x3C-5B :ASCII值从60到91的字符,即 < = > ? @ A-Z [
%x5D-7E :ASCII值从93到126的字符,即 ] ^ _ ` a-z { | } ~

除了上述字符外,其他所有控制字符(CTLs)、空白字符(空格、换行等)、双引号(DQUOTE)、逗号(comma)、分号(semicolon)和反斜杠(backslash)都不允许在Cookie值中出现。我在chrome中测试貌似只有分号不行,其他的好像可以,但是中文在请求时会乱码

如果Cookie的键值中包含空格、特殊符号或其他可能引起问题的字符,建议使用URL编码(encodeURIComponent)对其进行编码,确保Cookie在传输过程中不会出现问题。

Domain & Path

Domain 和 Path 一起标识了 Cookie 的作用域,即允许 Cookie 应该发送给哪些 URL。Domain 指定了哪些域名可以接受 Cookie,如果不指定,默认为当前页面所在域名(即:location.host),如果指定了 Domain,则一般包含子域名,所以该 cookie 的作用域将会更广。

Domain

假如我当前所在页面的地址为: test.cookie.com/ ,则默认的 Domain 为 test.cookie.com ,也可以将 Domain 的值设置为 .cookie.com,但不能设置为 new.test.cookie.com,更不能设置为顶级域名(.com)。需要注意的是 .cookie.com 前面的 .,这个至关重要,因为不加上这个点表示仅当前页面有效,不会在其子域名下共享,同时顶级域名不是一个具体有效的域名,因此 Domain 的值也不能设置为顶级域名。

通过 document.cookie="age=20;domain=cookie.com" 或 Set-Cookie: age=20;domain=cookie.com 设置是有效的,这时候浏览器会自动加上前缀 . ,如果直接在控制台的 Application 中修改 Domain 的值为 cookie.com 是会导致 cookie 失效的(该cookie将会在当前页面消失,在 cookie.com/ 页面中可以找到)。

下表是各个 Domain 值对应的域名生效对比:

Domain 参数 test.cookie.com new.test.cookie.com new.cookie.com cookie.com
test.cookie.com ✅ ❌ ❌ ❌
cookie.com ❌ ❌ ❌ ✅
.cookie.com ✅ ✅ ✅ ✅
.test.cookie.com ✅ ✅ ❌ ❌
Path

Cookie 的 Path 默认值是当前文档所在位置(即 location.pathname 最后一个斜杠前面的内容)。如果当前页面的 URL 是 https://cookie.com/example/test,那么默认情况下,该 Cookie 的 path 将是 /example,只有在路径以 /example 开头的页面才能访问这个 Cookie。下表是一些默认的 Path 的例子:

页面 URL Cookie Path
cookie.com/ /
cookie.com/example /
cookie.com/example/ /example
cookie.com/example/tes... /example
cookie.com/example?nam... /

如果 Path 不为 /,只有当页面 location.pathname 和 Path 值相匹配(location.pathname 的前缀是 Path 值,且 Path 值是 location.pathname 以某一个 / 分割的完整的路径)时才可以查看(控制台的 Application 中查看)和读写(document.cookie)对应的 Cookie。即当 Path 是 /example 时,页面 https://cookie.com/example 和页面 https://cookie.com/example/test 可以查看和读写,但是页面 https://cookie.com/ 和页面 https://cookie.com/examplefull 无法读写和查看的。

在请求时,也需要请求 URL 和 Path 匹配(URL 的前缀是 Path 值,且 Path 值是 URL 以某一个 / 分割的完整的路径),只有匹配时浏览器才会默认在 Cookie 请求头中携带对应的 cookie。

假设现在以下 Cookie:

  1. name1=zhangsan;path=/;
  2. name2=lisi;path=/test;
  3. name3=wangwu;path=/tes;

则在页面中有以下表现:

页面URL Cookie name1 Cookie name2 Cookie name3
cookie.com/ 可读写、可查看 不可读写、不可查看 不可读写、不可查看
cookie.com/test 可读写、可查看 可读写、可查看 不可读写、不可查看
cookie.com/test/new 可读写、可查看 可读写、可查看 不可读写、不可查看

请求时有以下表现(必需同源):

请求URL Cookie name1 Cookie name2 Cookie name3
/public 默认携带 不携带 不携带
/test 默认携带 默认携带 不携带

Expires/Max-age

Expires/Max-age 可以分为两个不同的属性 Expires 和 Max-age。这两个属性都是设置 Cookie 有效期的,Expires 的优先级比 Max-age 低,但是兼容性更好。如果 Expires 和 Max-age 都没有设置,则 Cookie 将在会话(Session)结束时过期。

Expires

Expires 指定的是 Cookie 的有效期,设置时标准值为 GMT 格式的时间戳。不规范的参数将会导致 Expires 属性被忽略。

js 复制代码
document.cookie="name=zhangsan;Expires=Tue, 29 Aug 2024 14:45:25 GMT";
// document.cookie="name=zhangsan;Expires=2023-08-02T16:13:29.910Z"; // 无效
// document.cookie="name=zhangsan;Expires=666"; // 无效
Max-age

Max-age 设置 cookie 的有效期,单位是秒,该属性不区分大小写。Max-age 的权重比 Expires 大,两个同时设置时,后者会被忽略。Max-age 的值必须是整数(设置成功后浏览器控制台中显示的值是一个具体的时间点),可以正负整数或0,如果设置不规范 Max-age 属性将会被忽略。当 Max-age 的值为:

  • 正整数:Cookie 的有效期是 当前时间 + Max-age。
  • 负数或0:Cookie 立即过期(即删除该Cookie)。
  • 不合法:Max-age 会被忽略,如果有 Expires,则 Cookie 的有效期由 Expires 指定,否则 Cookie 的有效期为 Session。
js 复制代码
document.cookie="name1=zhangsan;Max-age=60;Expires=Tue, 29 Aug 2024 14:45:25 GMT"; // 60秒后过期
document.cookie="name2=zhangsan;Max-age=0;Expires=Tue, 29 Aug 2024 14:45:25 GMT"; // 立即过期
document.cookie="name3=zhangsan;Max-age=a;Expires=Tue, 29 Aug 2024 14:45:25 GMT"; // Cookie 的过期时间为 Tue, 29 Aug 2024 14:45:25 GMT

Size

Cookie 的大小,由浏览器计算。

HttpOnly

HttpOnly 是包含在 Set-Cookie 响应头文件中的附加标志,表示禁止浏览器通过脚本读写该 Cookie,Documen.cookie API 无法对含有 httponly 标记的 Cookie 进行增删改查,主要是防止跨域脚本攻击(XSS)。

Secure

在 http 请求时,只有在浏览器认为安全的情况下才允许在请求时候携带 Cookie。下面是浏览器认为安全的三种情况:

  • 请求协议是 https
  • 请求的域名为 localhost
  • 请求的域名为 127.0.0.1

SameSite

Cookie 的 SameSite 属性用来限制第三方 Cookie,从而减少安全风险,该属性有三个值:

  • Strict: 最严格的,完全禁用第三方 Cookie,禁止任何跨站请求发送 Cookie。
  • Lax : Lax 规则稍稍放宽,大多数情况也是不发送第三方 Cookie,但是导航到目标网址的 Get 请求除外。协议或者端口不同但域名相同时(即同站时)也可以发送cookie。 mp.weixin.qq.com/s?__biz=Mzk...
  • None: 其他浏览器的默认值(chrome低版本的默认值也是 None),表示允许跨域请求访问该 Cookie (前提是请求时配置了允许携带)

在Chrome80+浏览器中,如果要设置 SameSite=None,前提是要设置 Secure=true, 否则 Cookie 就会不生效。

js 复制代码
// 在 Chrome v96 中
// Set-Cookie: name=zhangsan; SameSite=None // 无效,浏览器会警告
Set-Cookie: name=zhangsan; SameSite=None; Secure // 有效

Party Key

目前该属性还在提案阶段,可参考:github.com/cfredric/sa... 和 juejin.cn/post/700201...

Priority

这是一个很少用到的属性,因为到目前为止,该属性只有 Chrome 实现了。

内容节选自:asnokaze.hatenablog.com/entry/2019/...
Chrome于2013年实现了它(相应的commit),并于2016年提交了提议的规范" A Retention Priority Attribute for HTTP Cookies ",但尚未标准化。

该属性有三个值,分别是:High 、Medium 、Low ,用于指示 Cookie 的优先级,优先级越低越先被移除(每个域名下的 Cookie 数量是有限的,超过上限浏览器默认会优先移除最早添加的,加上优先级就默认先移除优先级最低的),默认值为:Medium

ts 复制代码
interface CookieOptions {
  maxAge: number;
  expires: string;
  path: string;
  domain: string;
  secure: boolean;
  sameSite: 'none' | 'lax' | 'strict';
}

// 设置 Cookie
function setCookie(name: string, value: string, options: CookieOptions) {
  let newCookie = `${name}=${encodeURIComponent(value)}`;
  if (options?.expires) {
    newCookie += `; expires=${options.expires}`;
  }
  if (options?.maxAge) {
    const expires = new Date(Date.now() + options.maxAge * 1000).toUTCString();
    newCookie += `; max-age=${options.maxAge}; expires=${expires}`;
  }
  if (options?.domain) {
    newCookie += `; domain=${options.domain}`;
  }
  if (options?.path) {
    newCookie += `; path=${options.path}`;
  }
  if (options?.secure) {
    newCookie += `; secure`;
  }
  if (options?.sameSite) {
    newCookie += `; SameSite=${options.sameSite}`;
  }
  document.cookie = newCookie;
}

// 获取 Cookie(由于document.cookie获取到的cookie是不区分domain和path的,所以无法获取指定domain或path的cookie值)
function getCookie(name: string): string | null {
  const cookies = document.cookie.split(';');
  console.log(cookies);
  for (let i = 0; i < cookies.length; i++) {
    const cookie = cookies[i].trim();
    if (cookie.startsWith(name + '=')) {
      return cookie.substring(name.length + 1);
    }
  }
  return null;
}

// 删除 Cookie
function deleteCookie(
  name: string,
  options?: { domain?: string; path?: string }
) {
  let newCookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; max-age=0`;
  if (options?.domain) {
    newCookie += `; domain=${options.domain}`;
  }
  if (options?.path) {
    newCookie += `; path=${options.path}`;
  }
  document.cookie = newCookie;
}

Cookie、localStorage、sessionStorage、IndexedDB的对比

下面是 Cookie、localStorage、sessionStorage 和 IndexedDB 的对比:

特点 存储容量 生命周期 跨会话 目的
Cookie 通常几KB 可设置过期时间 是 在客户端和服务器间传递少量信息,如用户认证信息、会话标识等
localStorage 通常几兆字节 永久存储 是 长期保存用户数据,如偏好设置、登录状态等
sessionStorage 通常几兆字节 当前会话期间 否 临时保存会话期间数据,如表单传递
IndexedDB 通常较大 永久存储 否 存储大量结构化数据,如离线应用、缓存数据等

总结:

  • 如果你需要在不同会话间共享数据,使用 localStorage 或 Cookie。
  • 如果你需要在当前会话期间共享数据,使用 sessionStorage。
  • 如果你需要在客户端存储较大的结构化数据,使用 IndexedDB。
相关推荐
三十而立洋3 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁3 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
李少兄3 天前
JavaScript 隐式全局变量解析
javascript
VIP_CQCRE3 天前
Coze 接入自定义模型更简单:用 Ace Data Cloud 打通 OpenAI Responses API
openai·api·ai agent·coze·acedatacloud
沙蒿同学3 天前
我用 Go 搭了一条 AI Agent 流水线:从 1 张商品图到一整套淘宝详情页
前端·javascript·后端
paopaokaka_luck3 天前
智慧社区综合服务小程序(人脸识别、AI问答、Echarts图形化分析)
前端·javascript·spring boot·spring·数据分析·echarts
Hilaku3 天前
为什么死磕 1px 的团队,用户体验反而更差?
前端·javascript·程序员
陈拉斯3 天前
给公司年会写了个大屏抽奖系统:动画在前端跑,凭什么说结果没被改?
javascript
yzy853 天前
Vue中下载或打开文件的JavaScript方法
前端·javascript·vue
用户298698530143 天前
在 React 中用 JavaScript 将 Word 转换为文本
前端·javascript·react.js