谈谈对 cookie 的理解

前言

我们知道 HTTP 属于无状态协议,在客户端和服务端数据传输完毕之后就会断开,再次请求时需要重新建立连接;这就使得服务器无法确认两次请求是否属于同一个客户端。

Cookie 的诞生最初就是为了解决:网站为了辨别用户身份而储存在用户本地终端上的数据。

使用流程大致如下:

在新的用户请求时,服务器给它颁发一个身份证存储在浏览器 cookie 下,当发起 http 请求时,浏览器会检查 cookie 并自动添加到 request header 的 cookie 字段中传输到服务器,这样服务器就会知道是谁来访问了。

早期,cookie 也被广泛应用于用户登录认证使用。

一、cookie 的使用

在前端,我们可以通过 document.domain 来获取当前网址域名,如掘金域名:'juejin.cn'

通过 document.cookie 可以获取该域下可以使用的所有 cookie,得到的是一个字符串类型:

'a=1; b=2; c=3'
'a=1; b=2; c=3'

其中 ; 来分割每一个 cookie,= 来分割 cookie 的 key 和 value。

通常我们会编写工具方法来解析我们想要的 cookie:

function getCookie(target) {
  const cookieList = document.cookie.split('; ');
  let result = '';
  for (let i = 0; i < cookieList.length; i++) {
    if (cookieList[i].includes(target)) {
      result = cookieList[i].split('=')[1];
      break;
    }
  }
  return result;
}
function getCookie(target) {
  const cookieList = document.cookie.split('; ');
  let result = '';
  for (let i = 0; i < cookieList.length; i++) {
    if (cookieList[i].includes(target)) {
      result = cookieList[i].split('=')[1];
      break;
    }
  }
  return result;
}

注意,尽管可以拿到 cookie 的 name 和 value 信息,但无法获取 cookie 的 domain 和 path 信息。

cookie 支持设置的属性有:

  1. expires 过期时间:
  • 如果不设置,则止为 session,即会话 cookie,在浏览器关闭后清除;
  • 如果设置一个已过期的时间,则 cookie 会被清除(删除动作);
  • 设置有效期可以通过下面方式来设置:
new Date(Date.now() + 864000).toUTCString();
new Date(Date.now() + 864000).toUTCString();
  1. domain 和 path:

domain 是域名,path 是路径,两者结合构成了 URL,这两者一起使用来限制 cookie 能够被哪些 url 访问获取。

domain 和 path 两个选项共同决定了 cookie 何时被浏览器自动添加到请求头部中发送出去。如果没有设置这两个选项,则会使用默认值。domain 的默认值为设置该 cookie 的网页所在的域名,path 默认值为设置该 cookie 的网页所在的目录。

  1. httpOnly:

用来设置 cookie 是否能通过 js 去访问。默认情况下设置 cookie 不带该选项,客户端可以通过 js 代码去访问(包括读取、修改、删除等)cookie。

当 cookie 设置了 HttpOnly 选项时,客户端将无法通过 JS 代码去访问(包括读取、修改、删除等)这个cookie。

另外,在客户端是不能通过 JS 代码去设置一个 httpOnly 类型的 cookie,这种类型的 cookie 只能通过服务端来设置。

  • 客户端设置 cookie:
document.cookie="key=value; expires=" + new Date(Date.now() + 864000).toUTCString() + "; domain=dogesoft.joywok.com; path=/";
document.cookie="key=value; expires=" + new Date(Date.now() + 864000).toUTCString() + "; domain=dogesoft.joywok.com; path=/";

注意,document.cookie 一次只能设置一个,如果你这样写:document.cookie="key1=value1; key2=value2;",只会存储 key1 到 cookie 中(第一个分号前面的信息视为 cookie 字段 key 和值),key2 会看作是 domain、path 这些配置属性,最终判做为无效属性。

  • 服务端设置 cookie:

服务端在返回 response 时可以通过 set-cookie 来设置,支持设置的属性有:expires、domain、path、HttpOnly

要想修改一个cookie,只需要重新赋值就行,旧的值会被新的值覆盖。这一点同「设置 cookie」

4. 删除 cookie:

删除 cookie 的方式上面已经提到,设置为过期时间即代表删除操作:

document.cookie="key=value; expires=" + new Date(Date.now() + -864000).toUTCString() + "; ";
document.cookie="key=value; expires=" + new Date(Date.now() + -864000).toUTCString() + "; ";

在设置 cookie 时满足哪些条件可以实现覆盖更新 cookie 值呢?需要满足 name、domain、path 都一致时表示去更新同一个 cookit。

那会不会有相同 name 的 cookie,存在多份情况呢?当同名 cookie 的 domain 或 path 存在不一致时会出现多份。

domain 是有共享特性的,即:cookie 子域名可以读父域名中的 cookie。

如在 .ping.com 域下注入 cookie,则该子域下的网页如 p1.ping.com、p2.ping.com 都能读取到 cookie 信息。

这会带来一个什么问题?

假设你现在访问 p1.ping.com 网址,现在 .ping.comp1.ping.com 下都有一个名称为 token 的 cookie。

不巧的是,其中有一个 token cookie 是失效的,而对于前端:

  • 一是无法获取指定 domain 下的 cookie,因为 document.cookie 拿到的只有 name 和 value,无法得知 cookie 的 domain 和 path 信息;
  • 二是通过 document.cookie 拿到多个 cookie 信息后,无法知道哪个可以使用,哪个是失效的;

当我们拿失效的 token 去请求接口后,得到响应是登录失效信息,这并不是我们想要的。

应对办法:

  1. 如果你有类似场景:页面是通过后台 redirect 重定向接口跳转过来,并且 token cookie 也是由 redirect 接口去埋入。

这种场景可以让后台在埋入 cookie 前将 「一级 domain」和「二级 domain」都先清除掉,再进行 cookie 的埋入。

  1. 另一种场景就是你的应用属于子应用,token 是从别的地方埋入的,你这里只有读取的逻辑。

可以将所有同名的 token cookie 收集起来,交给后台来校验 cookie 是否有效。

三、关于 domain 设置的方式

  1. 如果需要显式设置 domain,只能设置在当前域名和父级(一级)域名下,其他 domain 域名将设置不成功;
  2. 如果没有显式设置,则浏览器会自动取 url 的 host 作为 domain 值;
  3. 关于 domain value 前面带 . 点 与不带点的区别:
    • 带点:允许 subdomain 访问;
    • 不带点:只有完全一样的域名才能访问。

四、cookieStore

Chrome 87 版本之后的浏览器,为我们提供了操作 cookie 的 API:cookieStore,它是一个异步的 API,可以很方便的操作 Cookie。

并且原生 document.cookie 获取不到 cookie 的 domain 信息,它可以获取到。

使用时需要注意一点:只能在 https 协议下支持使用。

通常我们需要判断环境是否支持:

const isSupportCookieStore = location.protocol === 'https:' && typeof window.cookieStore === 'object';
const isSupportCookieStore = location.protocol === 'https:' && typeof window.cookieStore === 'object';
  • 获取单个 cookie:
await cookieStore.get('token');
await cookieStore.get('token');
  • 获取所有 cookie:
await cookieStore.getAll();
// or 
await cookieStore.getAll({ name: 'token' }});
// or 
await cookieStore.getAll('token');
await cookieStore.getAll();
// or 
await cookieStore.getAll({ name: 'token' }});
// or 
await cookieStore.getAll('token');
await cookieStore.set('cegz', true);
await cookieStore.set({ name: 'cyl', value: true });
await cookieStore.set('cegz', true);
await cookieStore.set({ name: 'cyl', value: true });
await cookieStore.delete({ name: 'token' });
await cookieStore.delete({ name: 'token' });

cookieStore 还提供了监听 cookie 变化的方式:

cookieStore.addEventListener('change', event => { 
  console.log(event.changed, event.deleted);
});
cookieStore.addEventListener('change', event => { 
  console.log(event.changed, event.deleted);
});

由于 cookieStore 兼容性很弱,再加上只能运行在 https 协议上,使得我们在真实业务上很难去投入生产使用,这里仅作为了解。