Open toFrankie opened 1 year ago
“Cookie” 这个词没有太多的含义,在计算机学科中很早就出现了,用来表示少量文本数据。
在前端领域,我认为 Cookie 的概念是很模糊的。有的时候,可以将它理解为一个小型文本,也可以理解为一种客户端(下称“浏览器”)与服务端(下称“服务器”)交互的一种存储机制。比如,同事 A 说让我看看那个 XXX Cookie 是多少,它具体指浏览器中存储的一小块文本。如果将 Cookie 与服务器 Session 放一起描述的时候,它指的是一种交互机制。
Cookie 是不安全的,原因是它本身没有采用任何加密机制。通过 HTTPS 来传输 Cookie 数据是安全的,它与 Cookie 本身无关,与 HTTPS 协议相关。
在 Web 中,Cookie 可被服务器、浏览器进行读写操作。在浏览器与服务器进行交互的过程中,浏览器会将任意 Cookie 写入 HTTP 头部,传输给服务端。随着 Web 的飞速发展,出于安全性的考虑,标准与浏览器都在往前走,因而并不是所有 Cookie 都会出现在 HTTP 头部。具体行为下文再谈。
Cookie 是一个小型文本文件,用于浏览器与服务器的数据传输。
我们都知道 HTTP 是无状态的。通俗地讲, 同一浏览器连续发送 HTTP 请求两次,服务器也无法识别到两个请求是来自同一客户端的。
但并不意味着,HTTP 协议的无状态是不好的。只是一些场景下,我们需要来维护“状态”(指的是客户端与服务端会话的状态,而不是说给 HTTP 协议加个状态,这是不对的,也无法做到)。
比如,网上购物、网络聊天、发布评论等场景,是需要维护状态的。试想一下,如果没有状态,你添加至购物车的商品,刷新下页面就不见了,那还不得急......解决方案也很多,比如给请求打上一个“标记”即可,在添加至购物车的请求上带上 userId 和 goodsId 并发送服务端,服务端拿到这些信息就可以区分来自哪个用户的了,并存储到相应的表。这里提到的 userId、goodsId 都是标记。
userId
goodsId
那一系列的问题来了,标记来自哪里、如何存储、发送请求如何带上、服务端如何接受?
Cookie
sessionStorage
localStorage
IndexedDB
Cache API
Cookie 仅仅只是其中一种存储方式而已,但它可以做到在服务端写、客户端读,然后发起请求时,自动携带在 HTTP 头部,服务端可以接收到。是一种可以做到无感的读写方案。同时,正是因为这种请求时自动带上的机制,被一些别有用心的人利用它来做一些坏事,比如 XSS、CSRF 等。
在这些存储方案里,Cookie 是最早出现的,所有浏览器都支持。随着 Web Storage 的发展,又出现了诸如 sessionStorage、localStorage 等方案。如果还不够用,还有可存储大量结构化数据的解决方案 IndexedDB。 随着 Web Storage 的普及,Cookie 将会回到最初的形态,作为一种被服务端脚本使用的客户端存储机制。
在这些存储方案里,Cookie 是最早出现的,所有浏览器都支持。随着 Web Storage 的发展,又出现了诸如 sessionStorage、localStorage 等方案。如果还不够用,还有可存储大量结构化数据的解决方案 IndexedDB。
随着 Web Storage 的普及,Cookie 将会回到最初的形态,作为一种被服务端脚本使用的客户端存储机制。
往下之前,可以先看下这篇文章:Cookie 和 Storage API 区别与详解。
Cookie 主要是用来服务服务器的,在浏览器里,每个域 Cookie 的空间限制不超过 4K,因而不适宜用于存储与服务器无关的数据。
4K
以下这些元数据,用来表示 Cookie 的文本数据、有效期、作用域、可访问性。
Name
Value
Domain
Path
Expires
Max-Age
Secure
HttpOnly
SameSite
先看看一个真实的 Cookie 吧,如下:
没什么好说的,对应 Cookie 的名称和数据,属于 Key-Value 键值对形式。Cookie 一旦创建,名称就不能更改了。Value 通常要进行编码处理。
Key-Value
Domain 决定了浏览器发出 HTTP 请求时,哪些域名会携带这个 Cookie。Path 则在 Domain 的基础上,进一步约束了该 Cookie 的可访问路径。
注意点:
window.location.host
document.domain
/
举个例子:
假设 Cookie 的 Domain 是 .example.com,那么发起 a.example.com 或 b.example.com 域名下的 HTTP 请求,都会携带该 Cookie,反之不行。
.example.com
a.example.com
b.example.com
假设 Cookie 的 Path 为 /user,那么请求路径为 /user/1 会带上;若请求路径为 / 或 /config 则不会带上。
/user
/user/1
/config
通俗地讲,
Domain:你是 A 公司的员工,那么 B 公司就不能使唤你。 Path:你是 C 部门的人,那么 D 部门就不能使唤你干活。
60 * 60 * 24 * 7
new Date(0)
0
Secure:
HttpOnly:
若 Cookie 指定了 HttpOnly 属性,那么这个 Cookie 将无法通过 JavaScript 脚本获取。只有在浏览器发出 HTTP 请求时才会携带上该 Cookie。这个主要是用来解决 XSS 攻击的。
在 Chrome 51 中引入这个 SameSite 属性,那时默认值为 None。从 Chrome 80 开始,默认值改为 Lax。
Cookie 的 SameSite 属性用来限制第三方 Cookie,从而减少安全风险。它可以设置三个值:
Strict
Lax
None
<a href="..."></a>
<link rel="prerender" href="..."/>
<form method="GET" action="...">
<form method="POST" action="...">
<iframe src="..."></iframe>
$.get("...")
<img src="...">
设置了 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 等信息进行攻击的。
优先级,这是 Chrome 的提案,定义了三种优先级,Low/Medium/High,当 cookie 数量超出时,低优先级的 cookie 会被优先清除。在 Safari 和 FireFox 中,不存在 Priority 属性。
不同浏览器有不同的清除策略:一些是替换掉最先(老)的 Cookie,有些则是随机替换。
前面不是提到 SameSite 会禁用第三方 Cookie 嘛,这个 SameParty 就是为了合法地使 Cookie 在一些第三方站点下也可使用(指的是发起 HTTP 请求时会带上)。它需要配合 First-Party Sets 策略使用。
Set-Cookie: name=frankie; Secure; SameSite=Lax; SameParty
这个是新特性,更多请看这里。
我们知道,Cookie 的读写是有差异的,读取的时候可以一次性获取当前作用域下的所用 Cookie,而写入的时候只能一条一条地进行写入操作。这跟浏览器与服务器之间 Cookie 的通信格式有关。浏览器向服务器发送 Cookie 的时候,是一行将所有 Cookie 全部发送。
关于 document.cookie 的读写差异,就是对象的 set/get 的原因。
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 } })
通过以下语句,可判断浏览器是否打开了 Cookie 功能,返回一个布尔值。
window.navigator.cookieEnabled // true 开启
若关闭了 Cookie 功能,无论是服务器 Set-Cookie,还是客户端通过 JS 脚本,都无法写入 Cookie。同样地,发起 HTTP 请求也将无法携带。
在服务端,各种后端语言或框架林立,写入 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 是无效的。
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。
未完待续...
Cookie 简述
Cookie 是什么?
“Cookie” 这个词没有太多的含义,在计算机学科中很早就出现了,用来表示少量文本数据。
在前端领域,我认为 Cookie 的概念是很模糊的。有的时候,可以将它理解为一个小型文本,也可以理解为一种客户端(下称“浏览器”)与服务端(下称“服务器”)交互的一种存储机制。比如,同事 A 说让我看看那个 XXX Cookie 是多少,它具体指浏览器中存储的一小块文本。如果将 Cookie 与服务器 Session 放一起描述的时候,它指的是一种交互机制。
Cookie 是不安全的,原因是它本身没有采用任何加密机制。通过 HTTPS 来传输 Cookie 数据是安全的,它与 Cookie 本身无关,与 HTTPS 协议相关。
在 Web 中,Cookie 可被服务器、浏览器进行读写操作。在浏览器与服务器进行交互的过程中,浏览器会将任意 Cookie 写入 HTTP 头部,传输给服务端。随着 Web 的飞速发展,出于安全性的考虑,标准与浏览器都在往前走,因而并不是所有 Cookie 都会出现在 HTTP 头部。具体行为下文再谈。
为什么需要 Cookie?
我们都知道 HTTP 是无状态的。通俗地讲, 同一浏览器连续发送 HTTP 请求两次,服务器也无法识别到两个请求是来自同一客户端的。
但并不意味着,HTTP 协议的无状态是不好的。只是一些场景下,我们需要来维护“状态”(指的是客户端与服务端会话的状态,而不是说给 HTTP 协议加个状态,这是不对的,也无法做到)。
比如,网上购物、网络聊天、发布评论等场景,是需要维护状态的。试想一下,如果没有状态,你添加至购物车的商品,刷新下页面就不见了,那还不得急......解决方案也很多,比如给请求打上一个“标记”即可,在添加至购物车的请求上带上
userId
和goodsId
并发送服务端,服务端拿到这些信息就可以区分来自哪个用户的了,并存储到相应的表。这里提到的userId
、goodsId
都是标记。那一系列的问题来了,标记来自哪里、如何存储、发送请求如何带上、服务端如何接受?
Cookie
、sessionStorage
、localStorage
,再有更新的IndexedDB
、Cache API
等接口。Cookie
仅仅只是其中一种存储方式而已,但它可以做到在服务端写、客户端读,然后发起请求时,自动携带在 HTTP 头部,服务端可以接收到。是一种可以做到无感的读写方案。同时,正是因为这种请求时自动带上的机制,被一些别有用心的人利用它来做一些坏事,比如 XSS、CSRF 等。Cookie 属性
Cookie 主要是用来服务服务器的,在浏览器里,每个域 Cookie 的空间限制不超过
4K
,因而不适宜用于存储与服务器无关的数据。以下这些元数据,用来表示 Cookie 的文本数据、有效期、作用域、可访问性。
Name
、Value
- 对应 Cookie 的名称和真实数据。Domain
、Path
- 决定谁可以读写这个 Cookie,类似于作用域。Expires
、Max-Age
- 决定了这个 Cookie 的寿命(有效期)。Secure
、HttpOnly
- 用来应对 XSS(跨站脚本攻击)。SameSite
- 用来应对 CSRF(跨站请求伪造)。先看看一个真实的 Cookie 吧,如下:
Name、Value
没什么好说的,对应 Cookie 的名称和数据,属于
Key-Value
键值对形式。Cookie 一旦创建,名称就不能更改了。Value 通常要进行编码处理。Domain、Path
Domain 决定了浏览器发出 HTTP 请求时,哪些域名会携带这个 Cookie。Path 则在 Domain 的基础上,进一步约束了该 Cookie 的可访问路径。
注意点:
window.location.host
或document.domain
的值。/
。举个例子:
假设 Cookie 的 Domain 是
.example.com
,那么发起a.example.com
或b.example.com
域名下的 HTTP 请求,都会携带该 Cookie,反之不行。假设 Cookie 的 Path 为
/user
,那么请求路径为/user/1
会带上;若请求路径为/
或/config
则不会带上。通俗地讲,
Expires、Max-Age
60 * 60 * 24 * 7
表示一周。注意点:
new Date(0)
。也可将 Max-Age 设为0
。Secure、HttpOnly
Secure:
HttpOnly:
若 Cookie 指定了 HttpOnly 属性,那么这个 Cookie 将无法通过 JavaScript 脚本获取。只有在浏览器发出 HTTP 请求时才会携带上该 Cookie。这个主要是用来解决 XSS 攻击的。
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 协议发送),否则无法显式关闭,即设置无效。<a href="..."></a>
<link rel="prerender" href="..."/>
<form method="GET" action="...">
<form method="POST" action="...">
<iframe src="..."></iframe>
$.get("...")
<img src="...">
设置了
Strict
或Lax
以后,基本就杜绝了 CSRF 攻击。当然,前提是用户浏览器支持SameSite
属性。设置为 Strict
设置为 Lax
设置为 None
额外地,Cookie 和 CSRF 的关系是什么?
CSRF 攻击,仅仅是利用了 HTTP 携带 Cookie 的特性进行攻击的,但是攻击站点还是无法得到被攻击站点的 Cookie。这个和 XSS 不同,XSS 是直接通过拿到 Cookie 等信息进行攻击的。
Priority
优先级,这是 Chrome 的提案,定义了三种优先级,Low/Medium/High,当 cookie 数量超出时,低优先级的 cookie 会被优先清除。在 Safari 和 FireFox 中,不存在 Priority 属性。
不同浏览器有不同的清除策略:一些是替换掉最先(老)的 Cookie,有些则是随机替换。
SameParty
前面不是提到 SameSite 会禁用第三方 Cookie 嘛,这个 SameParty 就是为了合法地使 Cookie 在一些第三方站点下也可使用(指的是发起 HTTP 请求时会带上)。它需要配合 First-Party Sets 策略使用。
Cookie 读写
我们知道,Cookie 的读写是有差异的,读取的时候可以一次性获取当前作用域下的所用 Cookie,而写入的时候只能一条一条地进行写入操作。这跟浏览器与服务器之间 Cookie 的通信格式有关。浏览器向服务器发送 Cookie 的时候,是一行将所有 Cookie 全部发送。
通过以下方式,可以窥探一二,可以知道它们都是函数。但由于是原生的内置方法,因此无法打印出具体的函数内容。
浏览器禁用 Cookie
通过以下语句,可判断浏览器是否打开了 Cookie 功能,返回一个布尔值。
若关闭了 Cookie 功能,无论是服务器 Set-Cookie,还是客户端通过 JS 脚本,都无法写入 Cookie。同样地,发起 HTTP 请求也将无法携带。
写入 Cookie
在服务端,各种后端语言或框架林立,写入 Cookie 的方式也各不相同。下面以 express 为例:
在客户端写入,只能通过
document.cookie
进行设置。注意,它只能为当前域写入 Cookie。假设你在个人网站设置:name=Frankie; Domain=github.com
是无效的。这样设置太麻烦了,我们通常会封装一个方法来添加、修改、删除、检查 Cookie,请看:Cookie.js。
未完待续...