📦 JavaScript 浏览器存储
# Cookie
Cookie 是一串 直接存储在浏览器的数据;是 HTTP 协议的一部分;
- 通常由 Web 服务器使用响应
Set-Cookie
HTTP header 设置。 - 浏览器使用
Cookie
HTTP header 自动添加到(几乎) 每个对相同域 的请求中。
🌰 例子 / 登录身份验证:
- 登录成功后,服务器在响应中使用
Set-Cookie
HTTP-header 来设置具有唯一的会话标识符的 cookie。- 下次当请求被发送到同一个域时,浏览器会使用
Cookie
HTTP-header 通过网络发送 cookie。- 服务器通过这个 cookie 识别身份的请求。
# 读取 cookie
使用 document.cookie
读取网站存储的 cookie。
cument.cookie
的值由name=value
对组成,以;
分隔。- 每一个都是独立的 cookie。
# 写入 cookie
使用 JavaScript 写入 cookie 使用
document.cookie
(不是数据属性),这个访问器 (包含getter
/setter
),对其赋值操作会被特殊处理。
对 document.cookie
的写入操作只会更新其中提到的 cookie,而不会涉及其他 cookie。
🌰 例子:
document.cookie = "user=john"; // 只更新名称为 user 的 cookie
console.log(document.cookie); // 展示所有的 cookie
2
cookie 的名称和值 可以是任何字符。为了保持有效的格式,它们应该使用内建的 encodeURIComponent
函数对其进行转义(与 URL 对象添加参数同理):
// 特殊字符(空格),需要编码
let name = "my name";
let value = "John Smith"
// 将 cookie 编码为 my%20name=John%20Smith
document.cookie = encodeURIComponent(name) + '=' + encodeURIComponent(value);
console.log(document.cookie);
2
3
4
5
6
7
8
cookie 存储限制:
encodeURIComponent
编码后的name=value
对,大小不能超过 4KB。- 每个域的 cookie 总数不得超过 20+ 左右,具体限制取决于浏览器。
Cookie 中的要设置的选项参数:
document.cookie = "user=John; path=/; expires=Tue, 19 Jan 2038 03:14:07 GMT"
1
path
:url
路径前缀。必须是绝对路径,使得该路径下的页面运行访问该 cookie。默认为当前路径。通常设置为path=/
,即使 cookie 对该网站的所有页面可见。例子,一个 cookie 带有
path=/admin
设置,那么该 cookie 在/admin
和/admin/something
下都是可见的,但是在/home
或/adminpage
下不可见。domain
:控制可以访问 cookie 的域。在实际中,有一些安全限制无法设置的域,例如,无法从另一个二级域访问 cookie(other.com
永远不会收到在site.com
设置的 cookie),这使得 cookie 的敏感数据只能存储在一个站点上可用的 cookie 中。** 默认情况下,cookie 只有在设置的域下才能被访问到。 ** 注意,默认情况下,cookie 不会共享给 子域(例如,
forum.site.com
是site.com
的子域)。可以通过设置
domain
允许子域设置 cookie:当在site.com
设置 cookie 时,应该明确地将domain
选项设置为根域:domain=site.com
。那么,所有子域都可以访问到这样的 cookie。🌰 例子:
document.cookie = "user=John; domain=site.com"
1在子域
forum.site.com
中,可以访问该 cookie:alert(document.cookie); // 有 cookie user=John
1expires
/max-age
:控制 cookie 的过期事件。默认情况下, 不设置这两个参数的 cookie 在关闭浏览器后消失,即为 session cookie。可以通过设置这两个参数保持 cookie 在浏览器关闭后仍然存在。expire
:设置浏览器会清除掉该 cookie 的时间日期。🌰 例子:
expires=Tue, 19 Jan 2038 03:14:07 GMT
1日期格式必须为 GMT 时区格式。可以通过
date.toUTCString()
方法转换。🌰 例子 / 将 cookie 设置为 一天后到期:
let date = new Date(Date.now() + 86400e3); date = date.toUTCString(); document.cookie = "user=john; expires=" + date;
1
2
3如果将
expires
设置为过去的日期,那么 cookie 会被直接删除。max-age
:是expires
的替代选项,指明 cookie 的过期时间距离当前时间的秒数。(设置在设定的秒数之后过期)。🌰 例子 / 将 cookie 设置为 1 个小时后失效:
document.cookie = "user=John; max-age=3600";
1🌰 例子 / 设置为 0 或者负数,cookie 立即过期:
document.cookie = "user=John; max-age=0";
1secure
:设置 cookie 的安全协议。默认情况下,如果在
http://site.com
上设置了 cookie,那么该 cookie 也会出现在https://site.com
上,反之亦然。即此时的 cookie 不区分 HTTP 协议。如果一个 cookie 是通过
https://site.com
设置的,那么它不会在相同域的 HTTP 环境下出现,例如http://site.com
。所以,如果一个 cookie 包含绝不应该通过未加密的 HTTP 协议发送的敏感内容,那么就应该设置secure
标识。🌰 例子 / 设置为只能在当前协议下可访问的 cookie(如当前在 HTTPS 环境下):
document.cookie = "user=John; secure";
1samesite
:cookie 的另一个安全特性,可以防止 XSRF (跨网站请求伪造) 攻击。XSRF 攻击的例子:当用户登录了
bank.com
,有了当前网站的 cookie。浏览器会在每次请求时将其发送到bank.com
的服务器,以便识别身份执行敏感的操作。此时,在浏览器的另一个窗口访问另一个网站,该网站具有发送
bank.com/pay
请求的表单。当每次访问bank.com
网站时,浏览器都会发送 cookie ,即使请求是从不同的网站发送的。实际的网站不会允许这样的情况发生,所有由
bank.com
生成的表单都具有一个特殊的字段,(XSRF 保护 token)。恶意页面既不能生成,也不能从远程页面提取它。它可以在那里提交表单,但是无法获取数据。并且,网站bank.com
会对收到的每个表单都进行这种 token 的检查。(一种防止 XSRF 的方式)上面的方法,需要检查每个表单请求的 token 字段。使用 cookie 的
samesite
选项:samesite=strict
:默认情况(没有值时):除了来自同一个网站的请求,设置了samesite=strict
的 cookie 在其他网站域永远不会被发送。设置为
strict
后, XSRF 攻击是没有机会成功的,因为来自evil.com
的提交没有 cookie。因此,bank.com
将无法识别用户,也就不会继续进行付款。但是
strict
有不方便的地方:当从其他地方访问bank.com
连接时,同样也不能发送samesite=strict
的 cookie。可以通过设置两种不同的 cookie 解决 :一种用于「一般识别」;另一种用于进行敏感数据信息操作时的识别(设为strict
的 cookie)。这样用户从别处访问网站时同样可以识别身份;并且进行敏感操作时必须从同一个网站启动才能进行。samesite=lax
:比strict
轻松的 cookie 限制。 同样可以防止 XSRF 攻击,但是不会破坏用户的体验。与
strict
相似,从外部来到网站的,禁止浏览器发送 cookie,但是有例外的情况(符合以下两个条件):HTTP 方法为安全方法;(安全的 HTTP 方法一般用于读取数据,不会更改数据,例如 GET)
该操作执行顶级导航(会更改浏览器地址栏中的 URL 的操作)
通常成立。如果在
<iframe>
中执行就不是顶级;用于网络请求的 JavaScript 方法不会执行任何导航。
samesite=lax
所做的是基本上允许最常见的前往 URL 操作携带 cookie。(例如,从笔记中打开网站),但是来自另一个网站的网络请求 / 表单提交都不会携带 cookie。httpOnly:Web 服务器使用
Set-Cookie
header 来设置 cookie。并且,它可以设置httpOnly
选项。这个选项禁止任何 JavaScript 访问 cookie,即使用document.cookie
看不到此类 cookie,也无法对此类 cookie 进行操作。
# cookie 操作函数
编写常见的 cookie 操作函数,相对于使用 document.cookie
更加方便(可以使用相似第三方库引入使用):
getCookie(name)
:返回具有给定的name
的 cookie。如果没有找到则返回undefined
。function getCookie(name) { let matches = document.cookie.match(new RegExp( "(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + "=([^;]*)" )); return matches ? decodeURIComponent(matches[1]) : undefined; }
1
2
3
4
5
6setCookie(name, value, options)
:将 cookie 的name
设置为具有默认值path=/
(可以修改以添加其他默认值)和给定值value
。function setCookie(name, value, options = {}) { options = { path: '/', // 如果需要,可以在这里添加其他默认值 ...options }; if (options.expires instanceof Date) { options.expires = options.expires.toUTCString(); } let updatedCookie = encodeURIComponent(name) + "=" + encodeURIComponent(value); for (let optionKey in options) { updatedCookie += "; " + optionKey; let optionValue = options[optionKey]; if (optionValue !== true) { updatedCookie += "=" + optionValue; } } document.cookie = updatedCookie; } // 使用范例: setCookie('user', 'John', {secure: true, 'max-age': 3600});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27deleteCookie(name)
:删除 cookie。通过设置过期时间
max-age
为 0 或者负数实现:function deleteCookie(name) { setCookie(name, "", { 'max-age': -1 }) }
1
2
3
4
5
注意
当进行更新或删除一个 cookie 操作时,应该使用和设置 cookie 时相同的路径和域选项。
# 第三方 cookie
常见的第三方 cookie 用与跟踪和广告服务。它们被绑定在原始域上,因此 ads.com
可以在不同网站之间跟踪同一用户,如果这些网站都可以访问 ads.com
的话。
例如:
site.com
网站的一个页面加载了另外一个网站的 banner:<img src="https://ads.com/banner.png">
。- 与 banner 一起,
ads.com
的远程服务器可能会设置带有id=1234
这样的 cookie 的Set-Cookie
header。此类 cookie 源自ads.com
域,并且仅在ads.com
中可见。- 下次访问
ads.com
网站时,远程服务器获取 cookieid
并识别用户。- 用户从
site.com
网站跳转至另一个也带有 banner 的网站other.com
时,ads.com
会获得该 cookie,因为它属于ads.com
,从而识别用户并在他在网站之间切换时对其进行跟踪。
现代浏览器可能会禁用此类的 cookie:
- Safari 浏览器完全不允许第三方 cookie。
- Firefox 浏览器附带了一个第三方域的黑名单,它阻止了来自名单内的域的第三方 cookie。
对于加载来自第三方域的脚本中使用
document.cookie
设置 cookie,无论脚本来自何处,设置的 cookie 都属于当前网页的域而不是第三方 cookie。
# LocalStorage / SessionStorage
通过 Web 存储对象 localStorage
和 sessionStorage
在浏览器中保存 键 / 值对数据。在页面刷新( sessionStorage
) 或者浏览器重启之后( localStorage
)数据仍然可以保留在浏览器中。
相对于 cookie,Web 存储对象:
- Web 存储对象不会随每个请求被发送到服务器。所以可以存储更多的数据,并且浏览器允许保存至少 2 MB 的数据(或者更多),并且具有配置数据的设置;
- 服务器无法通过 HTTP header 操作存储对象,一切通过 JavaScript 完成进行。
- 存储绑定到源(域 / 协议 / 端口三者)。也就是说,不同协议或子域对应不同的存储对象,它们之间无法访问彼此数据。
- 键值
key
/value
必须为字符串; - 存储大小限制为 5 MB+。取决于浏览器;
- 没有过期时间(LocalStorage)
两种存储对象提供相同的方法和属性:
setItem(key, value)
:存储键 / 值对。getItem(key)
:按照键获取值。removeItem(key)
:删除键及其对应的值。clear()
:删除所有数据。key(index)
: 获取该索引下的 键名。length
:存储的内容的长度。
# localStorage
的使用
一般在所有同源的窗口之间, localStorage
数据可以共享。
🌰 例子 / 使用 localStorage
:
存储数据:
localStorage.setItem('test', 1)
1读取数据:
console.log(localStorage.getItem('test')) // 1
1
在重新打开浏览器的同一个页面(不同窗口)也可以获取该数据;在同一个源(域 / 端口 / 协议),URL 路径可以不同,都可以共享该数据。
🌰 例子 / 类对象形式的访问:
与使用一个普通对象一样使用 LocalStorage:
// 设置 key
localStorage.test = 2;
// 获取 key
alert( localStorage.test ); // 2
// 删除 key
delete localStorage.test;
2
3
4
5
6
7
8
但一般不建议这样使用:
键值对由用户生成,可以为任何的内容。如果设置与内置对象属性相同的键(如
length
/toString
),使用这种方式会出错,而使用setItem
/getItem
可以正常工作:let key = 'length'; localStorage[key] = 5; // Error,无法对 length 进行赋值
1
2存在
storage
事件,使用 类对象的形式修改数据不会触发;setItem
正常修改数据会触发。
🌰 例子 / 遍历键:
由于存储对象是不可迭代的。要获取所有的键(或者值):
使用遍历数组的方法:
for (let i = 0; i < localStorage.length; i++) { let key = localStorage.key(i) console.log(`${key}: ${localStorage.getItem(key)}`) }
1
2
3
4使用
for... in
循环方法:注意这种方法会像处理常规对象一样;遍历所有的键,同时也会输出一些不需要的内建字段。
for(let key in localStorage) { alert(key); // 显示 getItem,setItem 和其他内建的东西 }
1
2
3需要使用
hasOwnProperty
检查过滤掉原型中的字段:for(let key in localStorage) { if (!localStorage.hasOwnProperty(key)) { continue; // 跳过像 "setItem","getItem" 等这样的键 } console.log(`${key}: ${localStorage.getItem(key)}`); }
1
2
3
4
5
6或者使用
Object.keys
获取只属于自己的键,需要的遍历出来:let keys = Object.keys(localStorage) for (let key of keys) { console.log(`${key}: ${localStorage.getItem(key)}`) }
1
2
3
4
注意,存储的对象的 键 / 值必须为 字符串。 如果为其他类型(数值、对象),会被自动转换为字符串。
可以使用 JSON.stringfy()
存储对象:
localStorage.user = JSON.stringify({name: "John"});
读取的数据使用 JSON.parse()
转换:
let user = JSON.parse(localStorage.user)
console.log(user)
2
或者对整个存储对象进行字符化处理:
console.log(JSON.stringfy(localStorage, null, 2))
# sessionStorage
的使用
与 localStorage
的使用基本类似,使用频率较低。使用 sessionStorage
有以下的限制:
- 存储的数据只能保存在当前的会话(浏览器的标签页):
- 意味着相同的页面不同的窗口会有不同的存储;
- 但是在同一个页面下的
iframe
下的存储是相同的。
- 存储的数据在刷新后能保留,但是关闭或者重启浏览器不会保留。
# storage
事件
当 localStorage
或 sessionStorage
中的数据更新后, storage
事件就会被触发,它会有以下的属性:
key
:发生数据更改的键key
;(如果调用的是clear
,key
为null
)oldValue
:旧值;(新增数值为null
)newValue
:新值;(删除数值为null
)url
:发生数据更改的文档的url
;storageData
:发生数据更改的(localStorage
/sessionStorage
)对象;storageArea
:包含存储对象(sessionStorage
和localStorage
)具有相同的事件,所以event.storageArea
引用了被修改的对象。 允许同源的不同窗口交换信息。
并且, storage
事件会在 所有可以访问到存储对象的 window
对象 上触发,导致当前数据发生改变的 window
对象除外。
🌰 例子 / 当有两个窗口具有相同的页面, localStorage
在它们之间共享数据。如果两个窗口都监听 window.onstorage
事件,那么每个窗口都会对另一个窗口中发生的更新作出反应。
在其他文档对同一存储进行更新时会触发:
window.onstorage = event => { if (event.key != 'now') return; console.log(event.key + ':' + event.newValue + " at " event.url) } // 或者使用 window.addEventListener('storage', event => { // ... // })
1
2
3
4
5
6
7
8会捕捉到关于
key
为now
的数据的更新:localStorage.setItem('now', Date.now())
1
# IndexedDB
是一个浏览器内建的数据库,比 localStorage
强大得多。
- 支持多种类型的键,可以存储几乎任何类型的值;
- 支持事务可靠性;
- 支持键值的范围查询、索引;
- 相比于
localStorage
可以存储更大的数据;
IndexedDB 适用于 离线应用,可与 ServiceWorkers 和其他技术相结合使用。