神奇的网页记事本(一)— 浏览器存储技术之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.combar.example.com 就不属于 跨站 ,因为他们同属于 example.com 的子域名。这里也不能简单理解为二级域名相同,比如 foo.github.iobar.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 可以分为两个不同的属性 ExpiresMax-age。这两个属性都是设置 Cookie 有效期的,Expires 的优先级比 Max-age 低,但是兼容性更好。如果 ExpiresMax-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 ",但尚未标准化。

该属性有三个值,分别是:HighMediumLow ,用于指示 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 通常较大 永久存储 存储大量结构化数据,如离线应用、缓存数据等

总结:

  • 如果你需要在不同会话间共享数据,使用 localStorageCookie
  • 如果你需要在当前会话期间共享数据,使用 sessionStorage
  • 如果你需要在客户端存储较大的结构化数据,使用 IndexedDB
相关推荐
IT女孩儿35 分钟前
JavaScript--WebAPI查缺补漏(二)
开发语言·前端·javascript·html·ecmascript
@解忧杂货铺5 小时前
前端vue如何实现数字框中通过鼠标滚轮上下滚动增减数字
前端·javascript·vue.js
真的很上进10 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
噢,我明白了13 小时前
同源策略:为什么XMLHttpRequest不能跨域请求资源?
javascript·跨域
sanguine__13 小时前
APIs-day2
javascript·css·css3
关你西红柿子14 小时前
小程序app封装公用顶部筛选区uv-drop-down
前端·javascript·vue.js·小程序·uv
济南小草根14 小时前
把一个Vue项目的页面打包后再另一个项目中使用
前端·javascript·vue.js
小木_.14 小时前
【python 逆向分析某有道翻译】分析有道翻译公开的密文内容,webpack类型,全程扣代码,最后实现接口调用翻译,仅供学习参考
javascript·python·学习·webpack·分享·逆向分析
Aphasia31114 小时前
一次搞懂 JS 对象转换,从此告别类型错误!
javascript·面试
m0_7482565615 小时前
Vue - axios的使用
前端·javascript·vue.js