Skip to main content

浏览器本地存储 / 缓存

前端缓存主要分为 HTTP 缓存和浏览器本地存储。

其中 HTTP 缓存是在 HTTP 请求传输时用到的缓存,主要在服务器代码上设置;而浏览器本地存储则主要由前端开发在前端 JS 上进行设置。

缓存可以说是性能优化中简单高效的一种优化方式了。一个优秀的缓存策略可以缩短网页请求资源的距离,减少延迟,并且由于缓存文件可以重复利用,还可以减少带宽,降低网络负荷。

一、Cookie​

Cookie 是服务器发送给浏览器并保存在本地的一小块数据,它会在浏览器下次发起请求时携带给服务器,用于辨别用户身份。

可以把 Cookie 理解为一个存储在浏览器里的一个小小文本文件,它附着在 HTTP 请求上,在浏览器和服务器之间飞来飞去。它可以携带用户信息,当服务器检查 Cookie 的时候,便可以获取到客户端的状态。

Cookie 诞生之初就是为了解决 HTTP 的无状态请求(HTTP 协议自身不对请求和响应之间的通信状态进行保存),用来记录一些用户相关的状态,常用于:

  • 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)
  • 个性化设置(如用户自定义设置、主题等)
  • 浏览器行为跟踪(如跟踪分析用户行为,进行广告推荐等)

1、Cookie 的原理​

Cookie 的原理是在浏览器第一次发送请求后,服务器在响应头添加一个 Set-Cookie 选项,将 Cookie 放入响应请求中,浏览器会把拿到的 Cookie 保存起来,在第二次发送请求时一起发给服务器,以此来辨别用户身份。

2、Cookie 的不可跨域名性​

很多网站都会使用 Cookie,例如 Google 向客户端颁发 Cookie,Baidu 也会向客户端颁发 Cookie。那浏览器访问 Google 会不会也携带上 Baidu 颁发的 Cookie 呢?

不会,因为 Cookie 具有不可跨域名性。根据 Cookie 规范,浏览器访问 Google 只会携带 Google 的 Cookie,而不会携带 Baidu 的 Cookie。Google 也只能操作 Google 的 Cookie,而不能操作 Baidu 的 Cookie。

需要注意的是,虽然网站 images.google.com 与网站 www.google.com 同属于 Google,但域名不一样,二者同样不能互相操作彼此的 Cookie。

3、Cookie 的设置​

可以通过 HTTP response header 响应头里的 Set-Cookie 指定要存储的 Cookie 值。例如:

res.setHeader('Set-Cookie', `username=dorki; Expires=Wed, 22 Oct 2022 02:22:22 GMT`)

支持的属性如下:

属性作用说明
<cookie-name>=<cookie-value>Cookie 键值对
  • <cookie-name>不能包含以下分隔字符:( ) < > @ , ; : \ " / [ ] ? = 控制字符 空格 制表符
  • <cookie-value> 是可选的,需要包含在双引号中,不能包含控制字符、空格、双引号、逗号、分号、反斜线
Expires=<date>Cookie 的有效时间,过了该时间浏览器会自动删除 Cookie
  • 注意必须为 GMT 格式的日期(Wdy,DD-Mon-YYYY HH:SS GMT),可通过 new Date().toGMTString() 或者 new Date().toUTCString() 来获得;
  • 不设置过期时间则表示这个 Cookie 生命周期为浏览器会话期间,只要关闭浏览器窗口 Cookie 就会消失;
  • 如果把过期时间设为过去则 Cookie 也会被删除;
  • 注意设定的时间只与客户端相关,而非服务端。
Max-Age=<number>表示 Cookie 将被删除的剩余时间,单位为秒
  • 如果同时指定了 Expires 和 Max-Age,则 Max-Age 的值优先生效;
  • max-Age 为负数时,表示该 Cookie 只是一个会话性 Cookie;
  • max-Age 为 0 时,会立即删除这个 Cookie。
Domain=<domain-value>指定 Cookie 可以送达的主机名
  • 如果没有指定,则默认为当前文档访问地址(URL)中的主机(host)部分(不包含子域名)
  • 前面带点则任何 subdomain 都可以访问,例如淘宝首页设置的 Domain 为 .taobao.com,则a.taobao.com、b.taobao.com 都可以使用 Cookie;
  • 前面不带点则只有完全一样的域名才能访问。
Path=<path-value>指定 URL 路径,这个路径必须出现在要请求的资源的路径中才可以发送 Cookie 首部
  • 例如设置 Path=/docs,/docs/Web/ 下的资源会带 Cookie 首部,/test 则不会携带;
Secure设置 Secure 后,Cookie 只在 HTTPS 协议下才发送到服务器
  • 用于保护 Cookie 在浏览器和服务器间的传输过程中不被窃取和篡改;
  • 注意 localhost 不受此限制。
HttpOnly设置 HttpOnly 后,会阻止 JS 通过 document.cookie 属性访问 Cookie(包括读取、修改、删除等)
  • 可以有效防止 XSS 攻击;
  • 客户端不能通过 JS 代码去设置 HttpOnly 类型的 Cookie,需要通过服务端来设置。
SameSite=<samesite-value>让 Cookie 在跨站请求时不会被发送
  • 可以有效阻止 CSRF 跨站请求伪造攻击;
    • strict:仅允许一方请求携带 Cookie,当前网页 URL 与请求目标 URL 需要完全一致;
    • lax(默认):允许部分第三方请求携带 Cookie;
    • none:无论是否跨站都会发送 Cookie。

3-2、通过 document.cookie 设置​

  • 设置 Cookie
document.cookie = "username=dorki; expires=Wed, 22 Oct 2022 02:22:22 GMT";

支持的属性如下(具体说明同上面表格):

  • <cookie-name>=<cookie-value> (例如 'username=dorki')
  • domain=<domain-value> (例如 'domain=example.com')
  • path=<path-value> (例如 'path=/mydir')
  • expires=<date> (例如 'expires=Wed, 22 Oct 2022 02:22:22 GMT')
  • max-age=<number> (例如 'max-age=3600')
  • secure (例如 'secure')

可以在 Application 的 Cookie 面板查看生成的 Cookie:

3-3、通过 document.cookie 删除​

// 删除 Cookie 不必指定 Cookie 值,直接把 expires 参数设置为过去的日期即可
document.cookie = "username=; expires=Thu, 01 Jan 1970 00:00:00 GMT"

也可以右键直接删除:

点击查看 Node.js 获取及操作 Cookie

4、Cookie 的安全问题​

通过 HTTP 连接传递 Cookie 很容易被查看,而且可以通过 JS 的 document.Cookie 获取到用户的 cookie,因此如果 Cookie 传递的内容比较重要,则要使用加密的数据传输。

  • 设置 HttpOnly 属性,阻止 JS 访问和修改 Cookie,可以有效防止 XSS 攻击;
  • 设置 Secure 属性,使 Cookie 只在 HTTPS 协议下才发送到服务器。

方式一:

把用户对象(包含了用户 ID、用户名、是否登录..)序列化成字符串再加密存入 cookie。密钥是:客户端 IP + 浏览器 Agent + 用户标识 + 固定的私有密钥

当 Cookie 被窃取后,只要任一信息不匹配,就无法解密 Cookie,进而也就不能登录了。

但这样做的缺点是 IP 不能变动、频繁加密解密会加重 CPU 负担。

方式二:

将用户的认证信息保存在一个 Cookie 中,具体如下:

  • Cookie 名:UID。推荐进行加密,例如 MD5;
  • Cookie 值:登录名 | 有效时间 Expires | hash 值。其中 hash 值可以由 登录名 + 有效时间 Expires + 用户密码(加密后的)的前几位 + Salt(Salt 是保证在服务器端站点配置文件中的随机数)

这种方式的优点是:

  1. 即使数据库被盗,盗用者还是无法登录到系统,因为组成 Cookie 值的 salt 是保证在服务器站点配置文件中而非数据库;
  2. 如果账户被盗,用户修改密码,可以使盗用者的 Cookie 值失效;
  3. 如果服务器端的数据库被盗,通过修改 salt 值可以使所有用户的 Cookie 值失效,迫使用户重新登录系统;
  4. 有效时间 Expires 可设为 当前时间+过去时间(例如 2 天),这样可以保证每次登录的 Cookie 值都不一样,防止盗用者窥探到自己的 Cookie 值后作为后门,长期登录。

5、Cookie 的缺点​

  • 存储大小只有 4k;
  • 请求头上带着数据,增加请求数据量;
  • 原生 API 不友好,需要自行封装;
  • 一个浏览器针对一个网站最多存 20 个 Cookie,浏览器一般只允许存放 300 个 Cookie。

为了弥补 Cookie 的局限性,WebStorage 出现了。有了 WebStorage 后,Cookie 可以只作为客户端与服务器交互的通道,保持客户端状态。

6、Cookie 的替代方案​

JWT​

JSON Web Token(JWT)是一个自包含的信息包,可以用来存储用户标识以及认证信息。可以被用来代替 Session Cookie。和 Cookie 自动附加到每个 HTTP 请求的方式不一样,JWT 必须被 Web 应用明确指定附加到那个 HTTP 请求上。

HTTP 认证​

HTTP 包含基本认证以及摘要认证协议,利用这些协议只有在提供了正确的用户名和密码后才能访问到 Web 页面。如果服务端需要类似的认证信息来确保 Web 页面的访问权限,那么浏览器每次请求页面时都要发送这些认证信息。这些认证信息也可以用来追踪用户。

IP 地址​

有些用户可能会被基于访问页面的电脑 IP 地址追踪过,服务端知道当前正在运行浏览器的电脑的 IP 地址,理论上可以对这个 IP 地址关联一个用户 Session。

然后 IP 地址通常不是一个可靠的追踪 Session 或标识用户的方法。许多电脑设计的时候就是为了让一个单独用户使用的,例如办公 PC,家庭 PC 会在网络地址转换协议下共享一个公共的 IP 地址。而且某些系统,例如 Tor 设计时就是为了保持匿名性的,利用 IP 地址追踪用户显然不合适,也是不可能的。

查询字符串​

一个更精确的技术是基于 URL 中嵌入信息,这种方法由服务端在 Web 页面的所有链接中追加包含一个独立 Session 标识的查询字符串组成。当用户点击了其中一个链接,浏览器把查询字符串传给服务端,允许服务端识别用户维持状态。

这些类型的查询字符串非常像 Cookie,都包含任意的信息供服务端选择,都会随请求返回给服务端。不同的是,由于查询字符串是 URL 中的一部分,如果 URL 后面被重复发送,那么上面附加的相同信息将被发送到服务端,产生混乱。例如,用户的偏好信息被放在查询字符串中,当用户把这个 URL 通过邮件发给了另一个用户,那么这些偏好信息就会变成另一个用户的。

而且当同一用户从不同的源多次访问同个页面时,不能确保每次使用相同的查询字符串。例如,如果一个用户第一次通过一个页面的内部站点访问了一个页面,然后第二次又通过外部的搜索引擎访问到这个页面,查询字符串可能会不同。这时应该使用 Cookie,Cookie 可以是相同的。

另外,使用查询字符串还存在安全问题。在查询字符串中存储标识 Session 的数据可能导致 Session 固定攻击,Referer 日志攻击及其他安全漏洞。把 Session 标识转成 HTTP Cookie 更安全。

隐藏的表单字段​

另一种会话跟踪是使用隐藏域的 Web 表单,这个技术很像使用 URL 查询字符串去保存信息。通过 HTTP 的 GET 方法处理表单和使用 URL 查询字符串类似,因为 GET 方法会把表单字段作为查询字符串追加到 URL 后面。但大部分表单都是通过 HTTP 的 POST 方法处理,这样表单信息包括隐藏的字段都会在 HTTP 请求体中发送,这样既不是 URL 中的一部分,也不是 Cookie 的一部分。

从追踪的角度来看这种方式有两种好处:

  • 把追踪信息放在 HTTP 请求体中而不是 URL 中意味着它不会被普通用户察觉;
  • 当用户复制 URL 的时候不会复制到 Session 信息。
Window.name DOM 属性​

浏览器可以通过 JS 使用 window.name 存储一个相当大的数据(2-23M)该数据可以用来代替 Session Cookie,也是可以跨域的。与 JSON 对象一起使用来存储客户端上的复杂 Session 变量集合。

在某些方面,这种方法可能比 Cookie 更加方便,因为它的内容不会像 Cookie 那样在每次请求的时候自动的发送给服务端,所以它不易收到网络 Cookie 嗅探攻击。然而如果不采用特殊的方法保护数据,它很容易受到其他攻击,因为数据可以被在同个窗口或 Tab 中打开的其他站点获取到。

广告主标识码​

苹果使用的追踪技术称为 广告主标识码(IDFA)。这种技术会给每个购买苹果产品的用户分配一个唯一标识,这个唯一标识会被苹果网络广告系统使用,来确定用户正在查看或者回复的广告。

ETag​

因为浏览器会缓存 ETags,然后在后续的请求相同资源时返回,追踪服务器可以简单的复制从浏览器接受的任意 ETag 来确保 ETag 长久留存(跟持久化 Cookie 一样)。增加缓存头也可以加强 ETag 数据的保存。

在某些浏览器中可以通过清理缓存来清除 ETag 数据。

web 存储​

一些 Web 浏览器支持持久化机制,允许页面本地存储信息供以后使用。

HTML5 标准包含了一个 JS API Web storage:Local Storage 和 Session Storage。Local Storage 的行为和持久化 Cookie 类似,而 Session Storage 的行为和 Session Cookie 的行为类似,也就是 Session Storage 是绑定在一个单独的 Tab 或者窗口的生命周期中的(也就是页面 Session),而 Session Cookie 是针对整个浏览器的。

IE 支持在浏览器历史中持久化信息,在浏览器的收藏夹中,以一个 XML 格式存储,或者直接在页面中存储到硬盘。

一些 Web 浏览器插件也包含持久化机制。例如 Flash 有 Local shared object,Silverlight 有 Isolated storage。

浏览器缓存​

浏览器缓存也可以用来存储信息,利用这些信息也可以用来追踪用户。这项技术利用的真相是当浏览器判断出来缓存的已经是最新资源时可以利用缓存而不是重新从站点下载。

例如,一个站点托管了一个 JS 文件,这个 JS 文件可以给用户指定一个唯一标识(例如,var userId = 3243242)。只要用户访问之后,每次用户再访问这个页面时,这个文件都会从缓存中获取而不是从服务端获取。所以它的内容永远不会变。

浏览器指纹​

浏览器指纹是指浏览器配置信息的集合,例如版本号,屏幕分辨率,操作系统。指纹信息可以用来完全或者部分标识独立用户或者设备,即使 Cookie 已经被关闭了。

基本的 Web 浏览器配置信息一直都在被 Web 分析服务搜集为了精确的统计真实网络流量和不同类型的点击欺诈。在客户端脚本的帮助下,搜集更多的参数也是有可能的。

二、WebStorage​

webStorage 是本地存储,数据不是由服务器请求传递的,因此可以存储大量的数据,而不影响网站的性能。比如保存的一些用户数据,或从接口获取的一些短期内不更新的数据,都可以用 WebStorage 来存储。

HTML5 提供了两种在客户端存储数据的新方法:localStorage 和 sessionStorage,挂载在 window 对象下。

1、localStorage​

localStorage 用来将数据存在于浏览器内,直到用户清除浏览器缓存为止。

1-1、localStorage 的特点​

  • 存储大小达到 5M;
  • 数据永久保存;
  • API 友好。

1-2、localStorage 的设置、修改和删除​

// 保存数据到 localStorage
localStorage.setItem('myCat', 'Tom');

// 读取 localStorage 项
let cat = localStorage.getItem('myCat');

// 移除指定 localStorage 项
localStorage.removeItem('myCat');

// 移除所有 localStorage 项
localStorage.clear();

2、sessionStorage​

sessionStorage 用来将数据存于浏览器的一次会话,当会话关闭,数据将被清空。

sessionStorage 即便是相同域名下的两个页面,只要它们不在同一个浏览器窗口,那么它们的 sessionStorage 数据便无法共享,而 localStorage 在所有同源窗口中都是共享的,Cookie 在所有同源窗口中也都是共享的。

注意 sessionStorage 与 Session 不同。

2-1、sessionStorage 的特点​

  • 存储大小达到 5M;
  • 会话级别的浏览器存储;
  • API 友好。

2-2、sessionStorage 的设置、修改和删除​

// 保存数据到 sessionStorage
sessionStorage.setItem('myCat', 'Tom');

// 从 sessionStorage 获取数据
let data = sessionStorage.getItem('myCat');

// 从 sessionStorage 删除指定的数据
sessionStorage.removeItem('myCat');

// 从 sessionStorage 删除所有保存的数据
sessionStorage.clear();

三、IndexedDB​

IndexedDB 是一个运行在浏览器上的非关系型数据库,用于在客户端存储大量结构化数据。

该 API 使用索引来实现对该数据的高性能搜索,理论上来说,IndexedDB 没有存储上限,它不仅可以存储字符串,还可以存储二进制数据。

1、IndexedDB 的特点​

  • 键值对储存

IndexedDB 内部采用对象仓库(object store)存放数据。所有类型的数据都可以直接存入,包括 JS 对象。对象仓库中,数据以键值对的形式保存,每一个数据记录都有对应的主键,主键是独一无二的,不能有重复,否则会抛出一个错误。

  • 异步

IndexedDB 操作时不会锁死浏览器,用户依然可以进行其他操作,这与 localStorage 形成对比,后者的操作是同步的。异步设计是为了防止大量数据的读写,拖慢网页的表现。

  • 支持事务

IndexedDB 支持事务(transaction),这意味着一系列操作步骤之中,只要有一步失败,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况。

  • 同源限制

IndexedDB 受到同源限制,每一个数据库对应创建它的域名。网页只能访问自身域名下的数据库,而不能访问跨域的数据库。

  • 储存空间大

IndexedDB 的储存空间比 localStorage 大得多,一般来说不少于 250MB,甚至没有上限。

  • 支持二进制储存

IndexedDB 不仅可以储存字符串,还可以储存二进制数据(ArrayBuffer 对象和 Blob 对象)

2、IndexedDB 的常见操作​

  • 建立打开 IndexedDB
window.indexedDB.open("testDB")

控制台得到一个 IDBDatabase 对象(即 IndexedDB 对象)

  • 关闭 IndexedDB
function closeDB(db) {
db.close();
}
  • 删除 IndexedDB
function deleteDB(name) {
indexedDB.deleteDatabase(name)
}

3、与前几种缓存方式的区别​

存储大小生命周期与服务端通信
Cookie4kb在设置的有效期之前有效每次都会携带在 header 中,增加请求数据量。
localStorage5MB永久有效(除非手动清理)不参与
sessionStorage5MB会话关闭前有效不参与
indexedDB无限永久有效(除非手动清理)不参与

四、往返缓存 BFCache​

往返缓存又称 BFCache,是浏览器在前进后退按钮上为了提升历史页面的渲染速度的一种策略。该策略具体表现为:

  • 当用户前进新页面时,将原页面的浏览器 DOM 状态保存到 BFCache 中;
  • 当用户点击后退按钮时,将页面直接从 BFCache 加载,节省网络请求的时间。