Skip to content

你想知道的 HTTP Cookie 都在这 #26

@gauseen

Description

@gauseen

1 为什么有 Cookie

背景

客户端和服务器通过 HTTP 协议通信,它是一种无状态的协议,客户端每次发送请求时,首先要和服务器端建立一个连接,在请求完成后又会断开这个连接。这种方式可以节省传输时占用的连接资源,但同时也存在一个问题:每次请求都是独立的,服务器端无法判断本次请求和上一次请求是否来自同一个用户。

为了解决 HTTP 无状态的问题,Lou Montulli 在 1994 年的时候,推出了 Cookie。

什么是 Cookie

HTTP Cookie(也叫 Web Cookie 或浏览器 Cookie)是服务器发送到用户浏览器并保存在本地的一小块数据,浏览器会存储 Cookie 并在下次发起请求时携带。如下图示例:

image

注:根据 HTTP 协议的规定,每个域名下的 Cookie 总大小不能超过4KB(4096字节)。如果超过该限制,浏览器会自动截断 Cookie 内容。

2 如何种 Cookie

2.1 后端

image-1

服务端示例代码(node)

const https = require('https');
const path = require('path');
const fs = require('fs');

let cookieNum = 0;

const server = https.createServer(options, (req, res) => {
  if (req.headers.origin) {
    // 设置允许跨域的域名
    res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
    // 设置允许的请求方法
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
    // 设置允许的请求头
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type');

    res.setHeader('Access-Control-Allow-Credentials', true);
  }

  // 处理预检请求(OPTIONS请求)
  if (req.method === 'OPTIONS') {
    res.writeHead(200);
    res.end();
    return;
  }

  // 处理其他请求
  if (req.url === '/login' && req.method === 'GET') {
    // 设置一个名为 "my-cookie" 的 Cookie,值为 "abc-*",有效期为 1 小时
    res.setHeader(
      'Set-Cookie',
      `my-cookie=abc-${cookieNum++}; Max-Age=3600; HttpOnly; Domain=b.com; SameSite=None; Secure;`
    );
    res.end('Cookie has been set!');
  } else if (req.url === '/getList' && req.method === 'GET') {
    res.end();
  } else {
    res.writeHead(404);
    res.end();
  }
});

server.listen(3001, () => {
  console.log('Server is running on port 3001');
});

可在浏览器查看种完的 Cookie,如下:

image-2

注:只能种跟接口域名同站的 Cookie,比如 test.b.com/login 接口不能种 Domain=c.com 下的 Cookie。

image-3

2.2 Cookie 属性

Expires & Max-Age

两者都可以设置 Cookie 有效期,Max-Age 优先级更高。过期后 Cookie 就不会再跟着请求发送到服务器。也会自动删除 Chrome DevTools -> Application -> Cookies 过期的 Cookie。

Domain

控制 Cookie 在哪些域名下可见可访问,如果指定了一个 Cookie Domain=ele.me,则它与所有的子域名(help.ele.me)共享 Cookie。可以理解为 Cookie 遵守的是「同站」策略。

什么是同站:只要两个 URL 的 eTLD+1 相同即是「同站」,否则就是「跨站」,不需要考虑协议和端口。定义如下:

eTLD: (effective top-level domain) 有效顶级域名,如 .com、.me、.github.io、.top 等

eTLD+1: 有效顶级域名 + 二级域名,如 ele.me,taobao.com

Path

控制 Cookie 在哪些路径下可访问,默认值为 / 也就是所有接口路径都可访问

如:Path=/api/

https://ele.me/api/getList 会携带 Cookie

https://ele.me/abc/getList 不携带 Cookie

HttpOnly

阻止 JavaScript 通过 Document.cookie 属性读写 Cookie。通过document.cookieapi 无法读、写对应的 Cookie

Secure

只有 https 协议(localhost 不受此限制)的请求才会携带 Cookie

SameSite

限制跨站请求不发送 Cookie,这样可以在一定程度上防范跨站请求伪造攻击(CSRF)

Strict(严格): 只发送同站请求的 Cookie,不会发送任何形式的跨站 Cookie

Lax(宽松): 默认值,一般情况下只发送同站 Cookie,有一些特殊情况可以发送跨站 Cookie,需要满足以下条件可以发送跨站 Cookie:

  1. 必须是 GET 或者 HEAD 请求,不能是 POST 请求
  2. 必须是顶级导航请求,可以认为该请求会改变浏览器地址栏 URL,比如通过 a 标签跳转的请求

举例:先在 test.b.com 网站下用 test.b.com/login 接口种 Lax Cookie。然后打开 test.a.com 页面,如下图,该页面有个 a 标签,点击会跳转到 test.b.com,此时会携带 Lax Cookie,但不会携带 Strict Cookie

image-4

None: 跨站和同站请求均发送 Cookie。在设置这个属性值时,必须同时设置 Secure 属性,比如:SameSite=None; Secure

image-5
image-6

SameSite=None + Secure 属性的方式在浏览器禁用三方 Cookie 之前可用,如果浏览器禁用三方 Cookie 需要怎么设置呢?-- 需要配合 Partitioned 属性一起使用

Partitioned

在浏览器禁用三方 Cookie 之前SameSite=None; Secure方式可正常使用,禁用三方 Cookie 后需要配合 Partitioned 属性使用。浏览器开启禁用三方 Cookie 时,set cookie 报错,如下:

image-7

当 Set Cookie 增加 Partitioned 属性后,Cookie 种成功,如下:

image-8

对应域的请求也可以正常携带 Cookie,如下:

image-9

设置 Partitioned 属性之前

到这里你可能会疑问,为什么要增加 Partitioned 属性?不是多此一举吗?那我们看下之前有什么问题

隐私问题

三方 Cookie 可以跨站跟踪用户的行为,因此可能会泄露用户的个人信息和浏览习惯,比如:
用户先访问淘宝,网站会通过 gm.mmstat.com/arms.1.1 接口种一个 sca Cookie,然后用户点击感兴趣的商品也会有 gm.mmstat.com 埋点请求,同时会携带 sca Cookie,如下图:

image-10

之后用户打开天猫,也会携带淘宝网站生成的 sca Cookie,这样 gm.mmstat.com 对应的服务就有能力分析出来用户的行为路径,用户的行为习惯就会被追踪

image-11

安全问题

三方 Cookie 可能被黑客利用来进行恶意攻击,比如伪造跨站请求攻击(CSRF),示例如下:

image-12

加 Partitioned 属性之后

加 Partitioned 属性后,Chrome 内核(从 114 版本开始)的浏览器会对 Cookie 进行分区。如下图,站点 A 内嵌入了站点 C,站点 C 设置了一个带有 Partitioned 属性的 Cookie,这个 Cookie 将保存在一个专门用于站点 A 嵌入 C 时设置的 Cookie 分区中。只有 C 是 A 网站的子应用时才会发送该 Cookie。

这时有个新站点 B,内嵌了站点C,这时 C 站点接口不会携带上面在 A 站点设置的 Cookie

image-13

如果用户直接访问 C 网站,C 网站的请求不会携带嵌入在 A 中设置的分区 Cookie,这样既能保证当前页面三方 Cookie 的可用性,也能避免上面提到的隐私问题、安全问题。

image-14

前端

前端种 Cookie 代码如下:

document.cookie = 'my-js-cookie=abc; Max-Age=3600; Path=/; Domain=b.com; SameSite=None; Secure;'

image-15

局限性

  • 只能种跟当前页面域名同站的 Cookie
    比如,在 test.a.com 页面下,document.cookieapi 只能种 Domain=a.com 下的 Cookie,不能种 Domain=b.com 下的 Cookie。

  • 不能重写有 HttpOnly 属性的同名 Cookie
    线上故障 Case:某App iOS 端 默认向 webview 容器里种 sid Cookie 作为登录态,并且指定 HttpOnly 属性,但切换账号后容器并没有更新 sid,H5 使用 document.cookie api 也无法重写 sid,导致内嵌的 H5 页面鉴权失败。如果使用后端接口种 Cookie 就没有这个问题。

  • 安全问题,跨站请求伪造攻击(CSRF)

浏览器禁用三方 Cookie 计划

Chrome 禁用三方 Cookie

从 Chrome 114 版本开始,无痕模式默认开启禁用三方 Cookie。

image-16
image-17

下面是 Chrome 禁用三方 Cookie 的计划

image-18

Safari 禁用三方 Cookie

目前(2023.11.8) Safari 浏览器默认禁用三方 Cookie,也不支持 Partitioned 属性,需要手动设置允许跨站 Cookie 才行,设置入口如下:

image-19

对现有业务影响

只要使用了三方 Cookie 并且没有 Partitioned 属性的业务都会受到影响。

三方 Cookie

  • Cookie Domain 与 页面 URL eTLD+1 不同

    eTLD+1: 有效顶级域名 + 二级域名,如 ele.me,taobao.com,如:test.a.com 页面下种了 Domain=b.com Cookie,则是三方 Cookie

  • 请求接口协议与页面协议不同也属于三方 Cookie

    如果页面域名和接口域名的 eTLD+1 相同,但页面域名是 http 协议,接口是 https 协议,也属于三方 Cookie

  • iframe 嵌入的三方子应用

    如果嵌入的 iframe 子应用域名与宿主应用域名不是同站,则它是第三方内容,三方网站内使用的 Cookie 都属于三方 Cookie。

    如下图,宿主页面(test.a.com)通过 iframe 嵌入子页面(test.b.com),子页面无法设置 Domain=b.com 域下的未分区的 Cookie。

    image-20

参考

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions