客户端数据存储技术:Cookie、Web Storage、Indexed DB、Cache API

Written by with ♥ on in 前端

随着技术的发展,客户端存储技术也在不断地进化,但在本质上都是为了解决如何在客户端高效读写数据的问题。

Cookie

在 Web 早期,网站应用既不能发 Ajax 异步请求,HTTP 又是无状态协议,网站又有维持状态的需求,那只能将状态放在 HTTP 方法中,手动将标识状态的 Token 放在表单或者 Query 字符串中都是非常容易出错的方式。有问题就要解决,当时网景公司的一名员工 Lou Montulli 在 1994 年将 magic cookies 的概念应用到 Web 通讯中,他的原始说明文档描述了 Cookie 工作的基本原理,该文档后来被纳入 RFC 2109规范(大多数浏览器实现的参考文档),最终被纳入 RFC 2965

简单说,Cookie 就是服务端通过 HTTP 的 Set-Cookie Header,存储在浏览器(客户端)的一小段文本,每个域名的 Cookie 最大为 4KB。当浏览器要发 HTTP 请求时,会检测本地是否有对应请求域的 Cookie,有则添加到 Request Header 中的 Cookie 字段中,浏览器会自动帮我们做这些事,免去了手动操作可能导致的错误。

前端技术发展到现在,其实 Cookie 没那么重要,要实现相同的功能,我们可以在每次 Ajax 请求中注入一个标识 Token 的 Request Header,在每次请求时发送。

同源限制

同源限制简单说就是只有相同的域才能共享 Cookie,浏览器不能把 baidu.com 的 Cookie 数据发到 google.com 的服务器,这样会泄露百度这个网站的 Cookie 产生安全问题。

  • 协议相同:HTTP 与 HTTPS 是不同的源
  • 域名相同:c.baidu.com 与 x.baidu.com 是不同的源
  • 端口相同:80 与 8080 端口是不同的源

只有同源的网页才能共享 Cookie,服务器可以在设置 Cookie 的时候,指定 Cookie 的所属域名为一级域名,比如 .example.com 这样的话,二级域名和三级域名不用做任何设置,都可以读取这个 Cookie。

服务端返回的 HTTP Response 中的 Set-Cookie Header 可以在客户端创建一个 Cookie 并通过各种不同的属性(属性之间使用分号和空格 ; 隔开)描述 Cookie 的行为,例如超时时间、所属的域、是否允许 js 读取、是否只在安全的协议下发送。

set-cookie: token=b7721d5476e9dd83a27fc547fcf36295; expires=Fri, 06-Sep-2019 03:12:54 GMT; Max-Age=2592000; path=/; Domain: .baidu.com

expires

设置 Cookie 的过期时间,必须是 GMT 格式的时间(new Date().toGMTString() 或 new Date().toUTCString() 获得

expires=Thu, 25 Feb 2018 04:18:00 GMT 表示 Cookie 在 2018 年 2 月 25 日 4:18 分失效,浏览器会清空失效的 Cookie。

如果没有设置 expires,默认有效期为 Session,即会话 Cookie,跟随浏览器的会话持续时间,关闭浏览器后 Cookie 失效。

expires 是 http 1.0 中的属性,在 http 1.1 中由 max-age 代替,两种的作用一样,只是语法不同。

max-age

max-age 是以秒为单位时间段,cookie失效时刻 = 创建时刻 + max-age,也就是存活多少秒。默认值为 -1,表示浏览器关闭就删除的会话 Cookie,0 表示马上失效(删除)。

domain

domain 限制 Cookie 被发送到哪些域,举个例子就明白的:

假设有一个 domain 为 .baidu.com 的 Cookie。如果请求的域名是 baidu.com、api.baidu.com 都会发送该 Cookie(一级域名包含二级域名),如果请求 google.com 就不会发送这个 Cookie。

domain 的默认值为设置该 Cookie 的网页所在域名。

如果是跨域 XHR 请求,即使 domain 和 path 都满足 Cookie 的 domain 和 path,默认情况下 Cookie 也不会添加到请求头中。

domain 可以设置为页面本域或父域,例如 www.baidu.com 可以设置 www.baidu.com 和 baidu.com 这两个域。

path

path 路径,限制 Cookie 被发生到哪些目录,/ 表示所有路径。

path 的默认值为设置该 Cookie 的网页所在目录。

secure

设置 Cookie 在确保安全的请求中才会发生,如 HTTPS 协议下才发送 Cookie,默认情况下,Cookie 不会带 secure 选项。

httponly

设置 Cookie 是否能通过 javaScript 访问,默认情况下,Cookie 不会带 HttpOnly 选项,可以通过 JavaScript 读取、修改、删除 Cookie。

console.log(document.cookie) // 读取所有 cookie,HttpOnly 的 cookie 不返回

samesite

可以设置为 lax 和 strict,预防 CSRF 攻击,这样只有在同一个站点的域下,才会发生 Cookie,从其他网站点击链接不会发送 Cookie。

strict 模式下所有的外部网站都不会发送,lax 模式下会在主域名的情况下发送。

创建 Cookie:

document.cookie = 'user_theme=dark; secure; samesite=lax'
document.cookie = 'user_lang=en-us'

HttpOnly 属于只能在服务端设置,JS 中可以设置除此以外的其他属性。

读取 Cookie:

console.log(document.cookie); // 'user_lang=en-us; user_theme=dark;'

解析 Cookie:

const parseCookies = x => x
  .split(';')
  .map(e => e.trim().split('='))
  .reduce((obj, [key, value]) => ({...obj, [key]: value}), {});

由于 Cookie 的接口不太友好,一般会使用 js-cookie 库操作 Cookie。

https://github.com/js-cookie/js-cookie

Cookies.set('name', 'value', { expires: 7 });

跨域

从安全角度考虑,浏览器无法获取跨域的 Cookie 是永远不会变的,跨域携带 Cookie 只有两种方法:

  • 通过 nginx 转发使用同一个域名
  • 前后台设置 withCredentials、Access-Control-Allow-Credentials

在 Web 页面中可以随意地载入跨域的图片、视频、样式等资源, 但 AJAX 请求通常会被浏览器应用同源安全策略,禁止获取跨域数据,以及限制发送跨域请求。虽然有多种方法利用资源标签进行跨域,但能够进行的数据交互非常有限。 在 2014 年 W3C 发布了 CORS Recommendation 来允许更方便的跨域资源共享。 默认情况下浏览器对跨域请求不会携带 Cookie,但鉴于 Cookie 在身份验证等方面的重要性, CORS 推荐使用额外的响应头字段来允许跨域发送 Cookie。

在初始化 XMLHttpRequest之后,设置 withCredentials = true 可让该跨域请求携带 Cookie(携带的是目标域名所在域的 Cookie)。

var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.withCredentials = true;
xhr.send();

Access-Control-Allow-Credentials

只设置客户端当然是没用的,还需要目标服务器接受你跨域发送的 Cookie。 否则会被浏览器的同源策略挡住:

服务器同时设置 Access-Control-Allow-Credentials 响应头为 “true”, 即可允许跨域请求携带 Cookie。

Access-Control-Allow-Origin

除了设置 Access-Control-Allow-Credentials 之外,跨域发送 Cookie 还要求 Access-Control-Allow-Origin 不允许使用通配符 *

事实上不仅不允许通配符,而且只能指定单一域名:

If the credentials flag is true and the response includes zero or more than one Access-Control-Allow-Credentials header values return fail and terminate this algorithm. –W3C Cross-Origin Resource Sharing

否则,浏览器还是会阻止跨域请求。

Web Storage API

Web Storage API 在 HTML5 添加,其中包括 localStoragesessionStorage 。相比 Cookie,新的 API 更适合保存客户端数据,其数据只会保存在本地,不会像 Cookie 一样每次都自动提交到服务端,有利于减小 HTTP 请求包的数据量,而且接口操作方式也更直观友好。

localStorage

不同浏览器的大小限制不同,一般是 5M 空间限制。

读写简单:

localStorage.setItem("key", "value")
localStorage.getItem("key") // value

// 保存对象
let user = {
    id: 1,
}
localStorage.setItem("key", JSON.stringify(user));
// 读取对象
JSON.parse(localStorage.getItem("key"));

删除数据:

localStorage.removeItem("key") // 删除某个 key
localStorage.clear() // 清除所有的数据

sessionStorage

sessionStorage 与 localStorage 的区别是:localStorage 没有过期时间,除非手动删除,否则数据会永久保留。而 sessionStorage 在页面会话结束时,存储在里面的数据会被清除。

存储的接口和 localStorage 是一样的。

同源策略

相同域名和端口的不同页面直接可以共享 localStorage 不能共享 sessionStorage,不同域名直接均不能共享 localStorage,如果想要在二级域名与主域名直接共享怎么办?

常用的方法是 postMessage 和 iframe 结合的方式,有兴趣可以自己搜索 localstorage的跨域存储方案

Indexed DB

浏览器内置的数据库系统,localStorage API 都是同步调用,Indexed DB 数据读写是异步调用,对数据的访问不会阻塞代码,而且可以存储通过结构化克隆算法复制的任何类型数据。

性能和灵活的代价是更复杂和低级的 API,有许多库提供更友好的 API:

  • localForage
  • PouchDB
  • idb
  • dexie

Cahce API

它最初是为 service workers 创建的,可用于永久缓存网络请求。

const apiRequest = new Request('https://www.example.com/items');
caches.open('exampleCache') // opens the cache
  .then(cache => {
    cache.match(apiRequest) // checks if the request is cached
      .then(cachedResponse => 
        cachedResponse || // return cachedReponse if available
        fetch(apiRequest) // otherwise, make new request
          .then(response => {
            cache.put(apiRequest, response); // cache the response
            return response;
          })
        })
    .then(res => console.log(res))
})

随后每次请求都会从缓存中读取数据。

参考文档