前言
浏览器本身常见的存储技术有:Cookie、localStorage、sessionStorage、IndexedDB、Web SQL Database、Cache Storage 等。
- Cookie :用于维持请求状态的小型文本文件,由网站通过用户的浏览器创建并存储。
- localStorage:localStorage 是 Web Storage API 的一部分,它是一种在浏览器中存储持久化数据的机制。它允许网页在用户的浏览器中保存键值对数据,并且这些数据在浏览器关闭后仍然有效,没有过期时间。
- sessionStorage:sessionStorage 也是 Web Storage API 的一部分,它是一种在浏览器中存储临时会话数据的机制。与 localStorage 不同,sessionStorage 中存储的数据仅在浏览器会话期间有效,即在用户关闭浏览器选项卡或窗口后,这些数据将被清除。
- IndexedDB: IndexedDB 是浏览器上的索引数据库,它允许网页在用户的浏览器上进行复杂的查询和事务操作。
- Web SQL Database: Web SQL Database 是一个基于 SQL 的浏览器数据库,可以在客户端存储结构化数据。虽然它曾经是 HTML5 的一部分,但目前已经被弃用,不再被推荐使用。现代浏览器更倾向于支持 IndexedDB
- Cache Storage: Cache Storage 是 Service Worker 的一部分,允许开发者对网络请求和响应进行缓存。它可以用于在离线状态下提供快速的资源访问,以及优化网页的性能和加载速度。
除了上面提到的存储技术,还有一些不常用的存储技术,我们打开控制台的 Application 就可以看到一系列和存储相关的选项:
本篇主要是讨论前面提到的第一种浏览器存储技术: Cookie
Cookie
Cookie 的作用不是为了存储数据,而是为了 维持状态 。我们都知道 http协议是无状态的 ,这就会导致每一次请求都是相互独立,不方便服务器识别用户身份和追踪用户行为,因此就诞生了 Cookie 技术,服务器可以设置和读取 Cookie 信息,以实现 会话跟踪 和 身份认证 等功能。一般来说 Cookie 是由服务端生成,由客户端进行存储和维护的,浏览器在发起 http 请求时,携带 Cookie 信息发送给服务器,由服务器辨别用户身份。
为了保护用户隐私,大多数主流浏览器都禁止 JavaScript 修改 Cookie 请求头。只有当请求与当前页面的源(协议、域名和端口)相同时,浏览器才会默认自动添加 Cookie 请求头并携带相应的 Cookie 信息。如果通过 js 设置 Cookie 请求头就会发生错误:Refused to set unsafe header "cookie"
Cookie 的生成方式
- 服务端生成。服务端根据 http 响应头的 set-cookie 设置 Cookie。
js
Set-Cookie: uid=fb3eda1aa35a9ed9f88f346a7; Path=/
- 客户端生成。客户端的 js 通过 document.cookie 也可以读写 Cookie。
js
document.cookie="name=张三";
document.cookie="age=18; domain=juejin.im; path=/";
Cookie 的限制
- 大小限制: 单个 Cookie 的大小通常限制在 4KB 左右,所以如果一个网站设置了的 Cookie 太大,可能会导致浏览器拒绝保存新的 Cookie。
- 数量限制 : 每个域名下的 Cookie 的个数是有上限,不同浏览器的上限不一样。如果一个网站设置了太多的Cookie,可能会导致浏览器覆盖旧的 Cookie。对于 Chrome(92) 浏览器每个 Cookie 字符串最长是 1024 个字符串(document.cookie 后面的字符串长度,包括 path、domain等,如果只有 name=value, 则 name=value 这个字符串长度最长为1024),超长的字符串会导致设置 cookie 失败。对于 Chrome 的同域名下的 cookie 个数最多为 176 个左右,不同版本可能都不太一样。
- 跨域限制:浏览器通常会限制来自其他域名的 Cookie 访问。当一个网页发起跨域请求时(例如使用XMLHttpRequest、Fetch API等),浏览器默认情况下不会在请求中发送跨域域名下的 Cookie 信息,即使在请求时手动增加请求头 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 的前提(必需全部满足):
- 前端配置 withCredentials(允许请求时携带凭据)。
- 原生 XMLHttpRequest :
(new XMLHttpRequest()).withCredentials = true;
。 - axios :
axios.defaults.withCredentials = true;
。 - fetch请求 :
fetch(url, { credentials: true })
。
- 原生 XMLHttpRequest :
- 前端页面的域名在后端允许跨域的白名单中(Access-Control-Allow-Origin 中包括前端页面所在域名)。
- 后端也需要允许请求时携带凭据(响应头:
Access-Control-Allow-Credentials:true
)。 - 在新版(v80+)的 Chrome 浏览器中,只有指定 Cookie 的 SameSite 属性为 None 且 Secure 属性为 true 才可以设置第三方 Cookie(因为chrome80+中,samesite默认值是lax);如果域名相同,但是端口或者协议不同 samesite 的值也可以是lax。
Cookie 各个键的说明
从下图可以看出 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:
- name1=zhangsan;path=/;
- name2=lisi;path=/test;
- 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
封装的操作 Cookie 的方法
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
。