正文
一、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 协议加个状态,这是不对的,也无法做到)。
比如,网上购物、网络聊天、发布评论等场景,是需要维护状态的。试想一下,如果没有状态,你添加至购物车的商品,刷新下页面就不见了,那还不得急......解决方案也很多,比如给请求打上一个“标记”即可,在添加至购物车的请求上带上 userId
和 goodsId
并发送服务端,服务端拿到这些信息就可以区分来自哪个用户的了,并存储到相应的表。这里提到的 userId
、goodsId
都是标记。
那一系列的问题来了,标记来自哪里、如何存储、发送请求如何带上、服务端如何接受?
- 标记一般来自服务端
- 客户端存储标记的方式有很多,比如常见的有:全局变量、
Cookie
、sessionStorage
、localStorage
,再有更新的IndexedDB
、Cache API
等接口。
Cookie
仅仅只是其中一种存储方式而已,但它可以做到在服务端写、客户端读,然后发起请求时,自动携带在 HTTP 头部,服务端可以接收到。是一种可以做到无感的读写方案。同时,正是因为这种请求时自动带上的机制,被一些别有用心的人利用它来做一些坏事,比如 XSS、CSRF 等。
在这些存储方案里,
Cookie
是最早出现的,所有浏览器都支持。随着 Web Storage 的发展,又出现了诸如sessionStorage
、localStorage
等方案。如果还不够用,还有可存储大量结构化数据的解决方案 IndexedDB。随着 Web Storage 的普及,Cookie 将会回到最初的形态,作为一种被服务端脚本使用的客户端存储机制。
二、Cookie 属性
Cookie 主要是用来服务服务器的,在浏览器里,每个域 Cookie 的空间限制不超过 4K
,因而不适宜用于存储与服务器无关的数据。
以下这些元数据,用来表示 Cookie 的文本数据、有效期、作用域、可访问性。
Name
、Value
- 对应 Cookie 的名称和真实数据。Domain
、Path
- 决定谁可以读写这个 Cookie,类似于作用域。Expires
、Max-Age
- 决定了这个 Cookie 的寿命(有效期)。Secure
、HttpOnly
- 用来应对 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.host
或document.domain
的值。 - 若没有指定 Path 的话,默认为
/
。 - 另外,在百度的页面下,不能将 Domain 设置成阿里的。即使不会报错,但它无效的,浏览器会自动忽略。
举个例子:
假设 Cookie 的 Domain 是 .example.com
,那么发起 a.example.com
或 b.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 协议发送),否则无法显式关闭,即设置无效。
请求类型 | 示例 | None | Lax |
链接 | <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 | 不发送 |
设置了 Strict
或 Lax
以后,基本就杜绝了 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。
未完待续...