Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
gauseen opened this issue Dec 1, 2023 · 0 comments
Open

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

gauseen opened this issue Dec 1, 2023 · 0 comments
Labels

Comments

@gauseen
Copy link
Owner

gauseen commented Dec 1, 2023

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

参考

@gauseen gauseen added javascript javascript HTTP labels Dec 1, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant