细读 JS | Cookie 详解

发布于 2023-05-12 18:26:23 字数 10931 浏览 69 评论 0

配图源自 Freepik

一、Cookie 简述

1. Cookie 是什么?

Cookie 这个词没有太多的含义,在计算机学科中很早就出现了,用来表示少量文本数据。

在前端领域,我认为 Cookie 的概念是很模糊的。有的时候,可以将它理解为一个小型文本,也可以理解为一种客户端(下称“浏览器”)与服务端(下称“服务器”)交互的一种存储机制。比如,同事 A 说让我看看那个 XXX Cookie 是多少,它具体指浏览器中存储的一小块文本。如果将 Cookie 与服务器 Session 放一起描述的时候,它指的是一种交互机制。

Cookie 是不安全的,原因是它本身没有采用任何加密机制。通过 HTTPS 来传输 Cookie 数据是安全的,它与 Cookie 本身无关,与 HTTPS 协议相关。

在 Web 中,Cookie 可被服务器、浏览器进行读写操作。在浏览器与服务器进行交互的过程中,浏览器会将任意 Cookie 写入 HTTP 头部,传输给服务端。随着 Web 的飞速发展,出于安全性的考虑,标准与浏览器都在往前走,因而并不是所有 Cookie 都会出现在 HTTP 头部。具体行为下文再谈。

Cookie 是一个小型文本文件,用于浏览器与服务器的数据传输。

2. 为什么需要 Cookie?

我们都知道 HTTP 是无状态的。通俗地讲, 同一浏览器连续发送 HTTP 请求两次,服务器也无法识别到两个请求是来自同一客户端的。

但并不意味着,HTTP 协议的无状态是不好的。只是一些场景下,我们需要来维护“状态”(指的是客户端与服务端会话的状态,而不是说给 HTTP 协议加个状态,这是不对的,也无法做到)。

比如,网上购物、网络聊天、发布评论等场景,是需要维护状态的。试想一下,如果没有状态,你添加至购物车的商品,刷新下页面就不见了,那还不得急......解决方案也很多,比如给请求打上一个“标记”即可,在添加至购物车的请求上带上 userIdgoodsId 并发送服务端,服务端拿到这些信息就可以区分来自哪个用户的了,并存储到相应的表。这里提到的 userIdgoodsId 都是标记。

那一系列的问题来了,标记来自哪里、如何存储、发送请求如何带上、服务端如何接受?

  • 标记一般来自服务端
  • 客户端存储标记的方式有很多,比如常见的有:全局变量、CookiesessionStoragelocalStorage,再有更新的 IndexedDBCache API 等接口。

Cookie 仅仅只是其中一种存储方式而已,但它可以做到在服务端写、客户端读,然后发起请求时,自动携带在 HTTP 头部,服务端可以接收到。是一种可以做到无感的读写方案。同时,正是因为这种请求时自动带上的机制,被一些别有用心的人利用它来做一些坏事,比如 XSS、CSRF 等。

在这些存储方案里,Cookie 是最早出现的,所有浏览器都支持。随着 Web Storage 的发展,又出现了诸如 sessionStoragelocalStorage 等方案。如果还不够用,还有可存储大量结构化数据的解决方案 IndexedDB

随着 Web Storage 的普及,Cookie 将会回到最初的形态,作为一种被服务端脚本使用的客户端存储机制。

二、Cookie 属性

插个话,阅读以下章节之前, 建议先看下这篇文章:Cookie 和 Storage API 区别与详解

Cookie 主要是用来服务服务器的,在浏览器里,每个域 Cookie 的空间限制不超过 4K,因而不适宜用于存储与服务器无关的数据。

以下这些元数据,用来表示 Cookie 的文本数据、有效期、作用域、可访问性。

  • NameValue - 对应 Cookie 的名称和真实数据。

  • DomainPath - 决定谁可以读写这个 Cookie,类似于作用域。

  • ExpiresMax-Age - 决定了这个 Cookie 的寿命(有效期)。

  • SecureHttpOnly - 用来应对 XSS(跨站脚本攻击)。

  • SameSite - 用来应对 CSRF(跨站请求伪造)。

先看看一个真实的 Cookie 吧,如下:

1. Name、Value

没什么好说的,对应 Cookie 的名称和数据,属于 Key-Value 键值对形式。Cookie 一旦创建,名称就不能更改了。Value 通常要进行编码处理。

2. Domain、Path

Domain 决定了浏览器发出 HTTP 请求时,哪些域名会携带这个 Cookie。Path 则在 Domain 的基础上,进一步约束了该 Cookie 的可访问路径。

注意点:

  • 若没有指定 Domain 属性,则默认设为当前 URL 的域名,即对应 window.location.hostdocument.domain 的值。
  • 若没有指定 Path 的话,默认为 /
  • 另外,在百度的页面下,不能将 Domain 设置成阿里的。即使不会报错,但它无效的,浏览器会自动忽略。

举个例子:

假设 Cookie 的 Domain 是 .example.com,那么发起 a.example.comb.example.com 域名下的 HTTP 请求,都会携带该 Cookie,反之不行。

假设 Cookie 的 Path 为 /user,那么请求路径为 /user/1 会带上;若请求路径为 //config 则不会带上。

通俗地讲,

Domain:你是 A 公司的员工,那么 B 公司就不能使唤你。
Path:你是 C 部门的人,那么 D 部门就不能使唤你干活。

3. Expires、Max-Age

  • 若没有指定 Expires 和 Max-Age 时,这个 Cookie 属于 Session Cookie,退出浏览器就会被清除。
  • Expires 用于设置 Cookie 的过期时间,它的值是 UTC 格式。
  • Max-Age 用于指定从现在开始 Cookie 存在的秒数,比如:60 * 60 * 24 * 7 表示一周。
  • 若同时指定 Expires 和 Max-Age,那么 Max-Age 优先生效。
  • 若到了失效时间,浏览器会自动清除相应的 Cookie。

注意点:

  • Session Cookie 退出浏览器时,有些浏览器不一定会清除,原因请看这里
  • Expires 依赖于本地系统时间,因此是不可靠的。而 Max-Age 则与本地时间无关,它总会在设置完的多少秒之后失效。
  • 若想通过脚本去删除某个 Cookie,可以将 Expires 设为过去的时间,比如 new Date(0)。也可将 Max-Age 设为 0
  • 将 Max-Age 设为负数,也相当于会话级 Cookie。
  • 在 HTTP/1.0 是通过 Expires 来判断的,而 HTTP/1.1 则通过 Max-Age 来判断,若无此参数,则降级使用 Expires 判断。

4. Secure、HttpOnly

Secure:

  • 若 Cookie 指定了 Secure,它只会在 HTTPS 请求中携带上。
  • 若当前协议是 HTTP,那么浏览器会自动忽略服务器发来的 Secure 属性。
  • Secure 只是一个开关,不需要指定值。如果通信协议是 HTTPS 协议,通过服务器写入的方式,将会自动打开。

HttpOnly:

若 Cookie 指定了 HttpOnly 属性,那么这个 Cookie 将无法通过 JavaScript 脚本获取。只有在浏览器发出 HTTP 请求时才会携带上该 Cookie。这个主要是用来解决 XSS 攻击的。

5. SameSite

在 Chrome 51 中引入这个 SameSite 属性,那时默认值为 None。从 Chrome 80 开始,默认值改为 Lax

Cookie 的 SameSite 属性用来限制第三方 Cookie,从而减少安全风险。它可以设置三个值:

  • Strict - 最为严格,完全禁止第三方 Cookie,跨站请求时,任何情况都不会发送 Cookie。只有当前页面 URL 与请求 URL 一致,才会带上 Cookie。
  • Lax - 规则稍稍放宽,大多数情况下,也是不发送第三方 Cookie,但是导航到目标网站的 GET 请求除外,只包括三种情况:链接,预加载请求,GET 表单(详见下表)。
  • None - 若浏览器的 SameSite 不为 None 时,网站可以显式关闭 SameSite 属性,将其设为 None。不过前提是必须同时设置 Secure 属性(Cookie 只能通过 HTTPS 协议发送),否则无法显式关闭,即设置无效。
请求类型示例NoneLax
链接<a href="..."></a>发送 Cookie发送 Cookie
预加载<link rel="prerender" href="..."/>发送 Cookie发送 Cookie
GET 表单<form method="GET" action="...">发送 Cookie发送 Cookie
POST 表单<form method="POST" action="...">发送 Cookie不发送
iframe<iframe src="..."></iframe>发送 Cookie不发送
AJAX$.get("...")发送 Cookie不发送
Image<img src="...">发送 Cookie不发送

设置了 StrictLax 以后,基本就杜绝了 CSRF 攻击。当然,前提是用户浏览器支持 SameSite 属性。

设置为 Strict

Set-Cookie: CookieName=CookieValue; SameSite=Strict;

设置为 Lax

Set-Cookie: CookieName=CookieValue; SameSite=Lax;

设置为 None

// ❌ 无效
Set-Cookie: widget_session=abc123; SameSite=None

// ✅ 有效
Set-Cookie: widget_session=abc123; SameSite=None; Secure

额外地,Cookie 和 CSRF 的关系是什么?

CSRF 攻击,仅仅是利用了 HTTP 携带 Cookie 的特性进行攻击的,但是攻击站点还是无法得到被攻击站点的 Cookie。这个和 XSS 不同,XSS 是直接通过拿到 Cookie 等信息进行攻击的。

6. Priority

优先级,这是 Chrome 的提案,定义了三种优先级,Low/Medium/High,当 cookie 数量超出时,低优先级的 cookie 会被优先清除。在 Safari 和 FireFox 中,不存在 Priority 属性。

不同浏览器有不同的清除策略:一些是替换掉最先(老)的 Cookie,有些则是随机替换。

7. SameParty

前面不是提到 SameSite 会禁用第三方 Cookie 嘛,这个 SameParty 就是为了合法地使 Cookie 在一些第三方站点下也可使用(指的是发起 HTTP 请求时会带上)。它需要配合 First-Party Sets 策略使用。

Set-Cookie: name=frankie; Secure; SameSite=Lax; SameParty

这个是新特性,更多请看这里

三、Cookie 读写

我们知道,Cookie 的读写是有差异的,读取的时候可以一次性获取当前作用域下的所用 Cookie,而写入的时候只能一条一条地进行写入操作。这跟浏览器与服务器之间 Cookie 的通信格式有关。浏览器向服务器发送 Cookie 的时候,是一行将所有 Cookie 全部发送。

关于 document.cookie 的读写差异,就是对象的 set/get 的原因。

通过以下方式,可以窥探一二,可以知道它们都是函数。但由于是原生的内置方法,因此无法打印出具体的函数内容。

// 这种方式已废弃
document.__lookupSetter__('cookie') // ƒ cookie() { [native code] }
document.__lookupGetter__('cookie') // ƒ cookie() { [native code] }

// 现在标准推荐用这个,但注意要 Document.prototype 上面找,因为它不会往原型上找的。
const descriptor = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie')
descriptor.set // ƒ cookie() { [native code] }
descriptor.get // ƒ cookie() { [native code] }

// 若要覆盖,可以这样写(实际项目中千万别重写,除非另起名称)
Object.defineProperty(document, 'cookie', {
  set: function () {
    console.log('custom setter method...')
    // do something...
  },
  get: function () {
    console.log('custom getter method...')
    // do something and return some value
  }
})

1. 浏览器禁用 Cookie

通过以下语句,可判断浏览器是否打开了 Cookie 功能,返回一个布尔值。

window.navigator.cookieEnabled // true 开启

若关闭了 Cookie 功能,无论是服务器 Set-Cookie,还是客户端通过 JS 脚本,都无法写入 Cookie。同样地,发起 HTTP 请求也将无法携带。

2. 写入 Cookie

在服务端,各种后端语言或框架林立,写入 Cookie 的方式也各不相同。下面以 express 为例:

import express from 'express'

const app = express()
const port = 8080

app.get('/', (req, res) => {
  // 通常 name 和 value 会经过编码处理的,像 express 默认采用 encodeURIComponent 进行编码处理,亦可在以下可选项中传入 encode 属性,它接受一个函数。
  res.cookie('cookie-name', 'cookie-value', {
    // 以下为可选项
    // domain, // 默认为 URL 对应域名(注意是服务器 URL)
    // path, // 默认为 /
    // expires, // 默认不设置,即会话级 Cookie
    // maxAge, // 默认不设置,即会话级 Cookie
    // secure, // 默认,根据请求协议决定是否开启。若 HTTP 请求,设置了 true,将无法写入浏览器
    // httpOnly, // 默认为 false
    // sameSite // 默认不设置,此时它的值取决于浏览器的默认值。比如 Chrome 80 之后,即使你在浏览器看到的是空的,但它的默认值为 Lax。
  })

  // 可写入多个
  res.cookie('other', '123')

  // other statements...
})

app.listen(port)

在客户端写入,只能通过 document.cookie 进行设置。注意,它只能为当前域写入 Cookie。假设你在个人网站设置:name=Frankie; Domain=github.com 是无效的。

document.cookie = 'name=Frankie; domain=*.example.com; path=/user; expires=Fri, 31 Dec 9999 23:59:59 GMT; max-age=3600; sameSite=Lax; secure; HttpOnly'

在浏览器里,通过 JS 脚本的方式,似乎无法写入 HttpOnly 的 Cookie。(待进一步验证)

这样设置太麻烦了,我们通常会封装一个方法来添加、修改、删除、检查 Cookie,请看:Cookie.js

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

0 文章
0 评论
24 人气
更多

推荐作者

wanghao

文章 0 评论 0

蓝天

文章 0 评论 0

handsomedeng

文章 0 评论 0

仙女

文章 0 评论 0

石海龙

文章 0 评论 0

dianjvnan

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文