谈谈 Cookies

1,354 阅读20分钟

Cookie 是我的这个系列的第一篇,本篇将不会对 cookie 的使用做讲解。

系列文章推荐: 什么是 JS

本期看点:

  1. 大家熟知的 cookie;
  2. cookie 的作用场景;
  3. 追踪用户行为的几种方式;
  4. 为什么有些网站可以读取本地的 cookie 来推广广告?
  5. cookie 的安全问题;

熟知的 Cookie

我们在学习 HTTP 的时候,便知道 http 协议请求是无状态的,通俗的说就是多次请求之间是互不相关的,服务器也就无法知晓多次请求是否来自同一个源。 作为只是用于简单的浏览信息的早期浏览器,是不存在交互的说法的。随着互联网的逐渐兴起(交互式Web兴起),HTTP 的无状态的弊端瞬间被放大。

Cookie 的概念最初被 Lou Montulli(Cookie的作者) 提出是在1993年,在1994年 cookie 的概念被开始应用于网络通信。而 Cookie 的诞生便是为了解决 HTTP无状态的特性无法满足交互式web 的!

作为网景公司的员工,虽然 Cookie 最初是因为解决一个在线商店关于用户购物信息问题的,但这一概念在以网景浏览器为背景的情况下,很快的在其他浏览器也渐渐开始支持Cookie,到目前所有浏览器便全部支持Cookie了。

在看过 Cookie 的前世今生之后,我们来具体的看一下 Cookie:

CokkiesLocal StorageSession Storage
Capacity4kb10mb5mb
Accessible fromAny windowAny windowSame tab
ExpiresManually setNeverOn tab close
StorageLocationBrowser and serverBorwser onlyBrowser only
Sent with requestsYesNoNo
BrowsersHTML4/5HTML5HTML5

首先我们要知道,Cookie 是直接存储在浏览器中的一小串数据。它们是 HTTP 协议的一部分,由 RFC 6265 规范定义。其次通过数据,我们可以看到 Cookie 在可以在任何窗口被访问,且时效是可以手动设置的,这也为我们提供了更多的操作空间。同时它本身也是存在一些限制的:尴尬的 4 Kb。

Cookies 中所保存的内容其实是由我们去自定义的,并且我们也可以对 Cookies 进行限制。具体 关于 cookie 的使用 可以查看:MDN,并且在MDN上为我们提供了一个小框架:一个完整支持 unicode 的 cookie 读取/写入器

由于 Cookies 只有 4Kb 的存储空间,所以在早期的开发人员发现这个限制满足不了需求,所以后来就有了一个很聪明的想法出现了:Cookie 将不再只是被呆板的去将数据存储在用户的本地电脑上,而是存储在自己的系统上,之后 Cookie 中存储的是数据信息便变成为了唯一的 ID,这样就不会被限制了。(其实我初次接触 cookie 就猜到了,cookie中存储应该是数据的地址。手动狗头) 这样一来cookie 将只是充当一个标识符的作用了,而不再是保存数据。不仅仅解决了存储限制问题,而且还可以通过标识在自己的系统中匹配到用户的数据。

我们来举个例子,理解一下上边的这段话:

我们手里有一张健身房的会员卡, 会员卡中的用户信息即:cookie 便作为我们身份的标识。这里,我们便类似于浏览器,健身房的服务台的电脑便类似于服务器,服务器通过 cookie 来对会员进行识别。

最后,Cookie 数据信息是不会放在客户端的 在极小数的情况下才会这么做 因为它存在一个弊端就是 空间不足。一般来说,我们会把会话的信息,放在服务器上这便是第三方cookie的概念了。

Cookie 的作用场景

  1. 我们在进行购物的时候,即使访问的同一个网址类似于某宝、狗东这样的购物平台,便会发现不同的用户看到的商品推荐是不一样的 这也是cookie做到的(它们其实都是在做一个跟踪用户的操作,它去收集用户的喜好 通过用户喜好来进行产品的推荐)

  2. 目前大部分网站都做了新用户注册这一项,有时注册了一下等到下次再访问该站点时,会自动识别到你并且向你问好。(你可能会离谱的觉着是公司请了心理咨询师,来从行为心理上去判断怎样做来吸引用户;你是否有过感觉被某些app“监视”。)其实很重要的是,网站是可以利用 cookie 跟踪统计用户访问该网站的习惯,比如什么时间访问,访问了哪些页面,在每个网页的停留时间等等。

  3. 你在某家航空公司站点查阅航班时刻表,该网站可能就创建了包含你旅行计划的 Cookies,也可能它只记录了你在该站点上曾经访问过的Web页,在你下次访问时,网站根据你的情况对显示的内容进行调整,将你所感兴趣的内容放在前列。这是高级的Cookie应用。

  4. 目前Cookie最广泛的是记录用户登录信息,这样下次访问时可以不需要输入自己的用户名、密码了,这其实都是cookie在本地做了保存。但是这种方便也存在用户信息泄密的问题,尤其在多个用户共用一台电脑时便更容易出现这样的问题。

追踪用户行为的几种方式

  • HTTP headers: 我们有些时候可以在 network 中通过响应头中,通过 referer 的这个字段反映的一些信息。 当前请求的这个页面的来源(来此页面之前的页面)而进行一些偏好推测从而展示相应的信息

  • 客户端的IP地址 进行追踪: 由于IP 地址是动态的、可伪造的,所以它并不稳定。也不能单单的通过 IP 来进行推测 ,所以说这种方式也并不可靠。

  • 通过用户登录: 通过用户的主动登记 这样就有的唯一的 ID 了,不过用户就多了一步操作了。(科技的进步往往是因为懒)

  • 胖url: 有些网站会为用户进行一些特定的url进行标识,在这个新的url中 会有一些特殊的字段作为用户的唯一标识(弊端:由于url比较长,会破坏一些公共数据的缓存,服务器的负荷增大并且随着浏览器的关系开启的再次访问 而又会产生新的标识)

  • Cookies: 这也是我们本章的核心, 服务器发送给用户浏览器并保存在本地的一小块数据。

    • Cookie可以保持登录信息到用户下次与服务器的会话,换句话说,下次访问同一网站时,用户会发现不必输入用户名和密码就已经登录了(当然,不排除用户手工删除Cookie)。而还有一些Cookie在用户退出会话的时候就被删除了,这样可以有效保护个人隐私。

    • Cookie在生成时就会被指定一个 Expire 或者设定 Max-age 值,这也是 Cookie 的生存周期,在这个周期内 Cookie 有效,超出周期 Cookie 就会被清除。有些页面将 Cookie 的生存周期设置为 “0” 或负值,这样在关闭浏览器时就会马上清除 Cookie,不会记录用户信息便更加安全。

为什么有些网站可以读取本地的 cookie 来推广广告?

比如说,我们去浏览一个网址但是我们并未进行包括点击访问的任何操作,此时的我们也就并未被网址保存 Cookies。

虽然当前的网址并不认识我们,但是这些广告网址是内嵌进来的,我们可能在其他对包含该广告的网址进行了访问,那我们的数据就可能在广告来源的或者其他地方被保存了相应的 Cookie 了。而这就将导致我们会被当前的网址因为广告而识别出来,从而通过 CookieID标识,在服务器上查找数据中我们的偏好信息以及行为,而进行动态的广告推送了。

这便是 Cookie 为用户提供个性化的服务的一种实践。另一方面也是作为了解所有用户行为的工具,对于网站经营策略的改进有一定参考价值。

Snipaste_2023-03-02_23-21-59.png

Cookie 的安全问题

为了内容的完整性,在这里还是不得不来提及到 Cookie 的几个选项属性。

从 document.cookie 说起:

Cookie 通常是由 Web 服务器使用响应 HTTP-header 设置的。然后浏览器使用 Cookie HTTP-header 将它们自动添加到(几乎)每个对相同域的请求中。

最常见的用处之一就是身份验证:

  1. 登录后,服务器在响应中使用 HTTP-header Set-Cookie 来设置具有唯一 “会话标识符(session identifier)” 的 cookie。
  2. 下次当请求被发送到同一个域时,浏览器会使用 Cookie HTTP-header 通过网络发送 cookie。
  3. 所以服务器知道是谁发起了请求。

我们还可以使用 document.cookie 属性从浏览器访问 cookie,查看你的浏览器是否存储了本网站的任何 cookie?让我们在控制台来看看:

alert( document.cookie ); // cookie1=value1; cookie2=value2;...
Snipaste_2023-03-03_09-48-01.png

document.cookie 的值由 name=value 对组成,以 ;  分隔。每一个都是独立的 cookie。

为了找到一个特定的 cookie,我们可以以 ;  作为分隔,将 document.cookie 分开,然后找到对应的名字。其实不介意通过正则的方式来做模式匹配,因为当键值存在相同的内容时,是不好判断的。我们可以通过 split 通过 ;和 = 做两次拆分即可。

我们可以通过 document.cookie 的方式写入 cookie,不同的浏览器在对于 cookie 的处理都是不同的。document.cookie 不是一个数据属性,它是一个 (访问器 getter/setter),对其的赋值操作都会被特殊处理。

对 document.cookie 的写入操作只会更新其中提到的 cookie,而不会涉及其他 cookie,通俗来讲:同键名的cookie数据,后者会覆盖前者。

例如,此调用设置了一个名称为 Params 且值为 cookies 的 cookie:

document.cookie = "Params=cookies"; // 只会更新名称为 user 的 cookie
alert(document.cookie); // 展示所有 cookie

如果你运行了上面这段代码,你会看到多个 cookie。这是因为 document.cookie= 操作不是重写整所有 cookie。它只设置代码中提到的 cookie Params

Snipaste_2023-03-03_10-00-39.png

从技术上讲,cookie 的名称和值可以是任何字符。为了保持有效的格式,它们应该使用内建的 encodeURIComponent 函数对其进行转义。

Tip: encodeURIComponent 存在一些限制:

  • encodeURIComponent 编码后的 name=value 的键值对,大小不能超过 4KB。因此,我们不能在一个 cookie 中保存大的东西。
  • 每个域的 cookie 总数不得超过 20+ 左右,具体限制取决于浏览器。

Cookie 有几个选项,其中很多都很重要,应该设置它。

选项被列在 key=value 之后,以 ; 分隔,像这样:

document.cookie = "user=John; path=/; expires=Tue, 19 Jan 2099 12:12:17 GMT"
// 关于格林威治时间,就是世界协调时间。

path: path=/mypath

url 路径的前缀必须是绝对路径,它使得该路径下的所有子页面可以访问该 cookie。默认为当前路径。

如果一个 cookie 带有 path=/admin 设置,那么该 cookie在 /admin 和 /admin/something 下都是可见的,但是在 /home 或 /child 下不可见。

通常,我们应该将 path 设置为根目录:path=/,以使 cookie 对此网站的所有页面可见。

domain: domain=site.com

domain 控制了可访问 cookie 的域。但是在实际中,有一些限制:我们是无法设置任何域的。(无法从另一个二级域访问 cookie,就是说 a.com 永远不会收到在 b.com 设置的 cookie。)

这是一项安全限制,为了允许我们将敏感数据存储在应该仅在一个站点上可用的 cookie 中。 默认情况下,cookie 只有在设置的域下才能被访问到。

需要注意的是:默认情况下 cookie 不会共享给子域,例如 :

// 如果我们在 baidu.com 网站上设置了 cookie……
document.cookie = "user=cookie"

// 在 pan.baidu.com 域下我们是无法访问的
alert(document.cookie); // 没有 user

但这是可以设置的。如果我们想允许像 pan.baidu.com 这样的子域在 pan.com 上设置 cookie,便可以通过domain 选项设置为根域。为此,当在 baidu.com 设置 cookie 时,我们应该明确地将 domain 选项设置为:domain=baidu.com。那么,所有子域都可以访问到这样的 cookie了。

// 在 baidu.com
// 使 cookie 可以被在任何子域 *.baidu.com 访问:
document.cookie = "params=cookies; domain=baidu.com"

// 之后在 pan.baidu.com 便可以看到 "params=cookies"
alert(document.cookie); 

出于历史原因,domain=.site.comsite.com 前面有一个点符号)也以相同的方式工作,允许从子域访问 cookie。这是一个旧的表示方式,如果我们需要支持非常旧的浏览器,那么便应该使用它。

总结一下,通过 domain 选项的设置,可以实现允许在子域访问 cookie。

Session: expires,max-age

默认情况下,如果一个 cookie 没有设置这两个参数中的任何一个,那么在关闭浏览器之后,它便会被浏览器清除。此类 cookie 被称为 "session cookie”。

我们可以通过设置 expires 或 max-age 选项中的任意一个,来让 cookie 可以在浏览器关闭后仍然存在。

  • expires=Tue, 19 Jan 2099 12:12:17 GMT

cookie 的过期时间定义了浏览器会自动清除该 cookie 的时间。日期必须完全采用 GMT 时区的这种格式。我们可以使用 date.toUTCString 来获取它。例如,我们可以将 cookie 设置为 1 天后过期。

// 当前时间 +1 天 
// 如果我们将 `expires` 设置为过去的时间,则 cookie 会被删除。
let date = new Date(Date.now() + 86400e3);
date = date.toUTCString();
document.cookie = "user=John; expires=" + date;

我们再来看 max-age,它是 expires 的替代选项,指明了 cookie 的过期时间距离当前时间的秒数。

  • max-age=3600
// cookie 会在一小时后失效
document.cookie = "user=John; max-age=3600";

// 删除 cookie(让它立即过期)
document.cookie = "user=John; max-age=0";

这正如我们在追踪用户行为的几种方式中提到: Cookie在生成时就会被指定一个 Expire 或者设定 Max-age 值,这也是 Cookie 的生存周期,在这个周期内 Cookie 有效,超出周期 Cookie 就会被清除。有些页面将 Cookie 的生存周期设置为 “0” 或负值,这样在关闭浏览器时就会马上清除 Cookie,不会记录用户信息便更加安全。

secure

可以通过 secure 来设置 Cookie 应只能被通过 HTTPS 协议进行传输。默认情况下,如果我们在 http://baidu.com 上设置了 cookie,那么该 cookie 也会出现在 https://baidu.com 上,反之亦然。 也就是说,cookie 是基于域的,它们不区分协议。

使用此选项,如果一个 cookie 是通过 https://baidu.com 设置的,那么它不会在相同域的 HTTP 环境下出现,例如 http://baidu.com。所以,如果一个 cookie 包含绝不应该通过未加密的 HTTP 协议发送的敏感内容,那么就应该设置 secure 标识。

// 假设我们现在在 HTTPS 环境下
// 设置 cookie secure(只在 HTTPS 环境下可访问)
document.cookie = "params=cookies; secure";

SAMESITE

这是另外一个关于安全的特性。它旨在防止 XSRF(跨网站请求伪造)攻击。为了了解它是如何工作的,以及何时有用,让我们看一下 XSRF 攻击。

XSRF 攻击

XSRF 是跨站请求伪造(Cross-site request forgery)的缩写,也被称为one-click attack 或者 session riding,又叫做 CSRF。是 一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。 -- 维基百科。

那什么是 XSRF 攻击?想象一下,你登录了 Amazon.com 网站。此时:你有了来自该网站的身份验证 cookie。你的浏览器会在每次请求时将其发送到 Amazon.com,以便识别你,并执行所有敏感的财务上的操作。

现在,在另外一个窗口中浏览网页时,你不小心访问了另一个网站 virus.com(virus:病毒)。该网站具有向 Amazon.com 网站提交一个具有启动与黑客账户交易的字段的表单 <form action="https://Amazon.com/pay"> 的 JavaScript 代码。(其实这里想用微信支付来着🤣)

你每次访问 Amazon.com 时,浏览器都会发送 cookie,即使该表单是从 virus.com 提交过来的。因此,银行会识别你的身份,并执行真实的付款。这就是所谓的“跨网站请求伪造(Cross-Site Request Forgery,简称 XSRF)”攻击。

Snipaste_2023-03-03_11-14-33.png

当然,实际的银行会防止出现这种情况。所有由 Amazon.com 生成的表单都具有一个特殊的字段,即所谓的 “XSRF 保护 token”,恶意页面既不能生成,也不能从远程页面提取它。它可以在那里提交表单,但是无法获取数据。并且,网站 Amazon.com 会对收到的每个表单都进行这种 token 的检查。

但是,实现这种防护需要花费时间。我们需要确保每个表单都具有所需的 token 字段,并且我们还必须检查所有请求。

输入 cookie samesite 选项

Cookie 的 samesite 选项提供了另一种防止此类攻击的方式,(理论上)不需要要求 “XSRF 保护 token”。

它有两个可能的值:

  • samesite=strict(和没有值的 samesite 一样)

如果用户来自同一网站之外,那么设置了 samesite=strict 的 cookie 永远不会被发送。换句话说,无论用户是通过邮件链接还是从 virus.com 提交表单,或者进行了任何来自其他域下的操作,cookie 都不会被发送。

如果身份验证 cookie 具有 samesite 选项,那么 XSRF 攻击是没有机会成功的,因为来自 virus.com 的提交没有 cookie 信息。因此,Amazon.com 将无法识别用户,也就不会继续进行付款。 这种保护是相当可靠的。只有来自 Amazon.com 的操作才会发送 samesite cookie,例如来自 Amazon.com 的另一页面的表单提交。

虽然,这样有一些不方便。

当用户通过合法的链接访问 Amazon.com 时,例如从他们自己的其他域而来,他们会感到疑惑,Amazon.com 无法识别他们的身份。实际上,在这种情况下不会发送 samesite=strict cookie。

我们可以通过使用两个 cookie 来解决这个问题:一个 cookie 用于“一般识别”,仅用于传递 “Params, cookies”,另一个带有 samesite=strict 的 cookie 用于进行数据更改的操作。这样,从网站外部来的用户会看到欢迎信息,但是支付操作必须是从银行网站启动的,这样第二个 cookie 才能被发送。

  • samesite=lax

这是一种更加宽泛的方法,该方法还可以防止 XSRF 攻击,并且不会破坏用户体验。 宽松(lax)模式,和 strict 模式类似,当从外部来到网站,则禁止浏览器发送 cookie,但是增加了一个例外。

如果以下两个条件均成立,则会发送含 samesite=lax 的 cookie:

  1. HTTP 方法是“安全的”(例如 GET 方法,而不是 POST)。

    所有安全的 HTTP 方法详见 RFC7231 规范。基本上,这些都是用于读取而不是写入数据的方法。它们不得执行任何更改数据的操作。跟随链接始终是 GET,是安全的方法。

  2. 该操作执行顶级导航(更改浏览器地址栏中的 URL)。

    这通常是成立的,但是如果导航是在一个 <iframe> 中执行的,那么它就不是顶级的。此外,用于网络请求的 JavaScript 方法不会执行任何导航,因此它们不适合。

所以,samesite=lax 所做的是基本上允许最常见的“前往 URL”操作携带 cookie。例如,从亚马逊的书城打开网站链接就满足这些条件。 但是任何更复杂的事儿,例如来自另一个网站的网络请求或表单提交都会丢失 cookie。

如果这种情况对于我们的工作需求更适合,那么添加 samesite=lax 将不会破坏用户体验并且可以增加保护。 总体而言,samesite 是一个很好的选项。

但它有个缺点:

  • samesite 会被到 2017 年左右的旧版本浏览器忽略(不兼容)。

因此,如果我们仅依靠 samesite 提供保护,那么在旧版本的浏览器上将很容易受到攻击。

但是,我们肯定可以将 samesite 与其他保护措施一起使用,例如 XSRF token,这样可以多增加一层保护,将来,当旧版本的浏览器淘汰时,我们可能就可以删除 xsrf token 这种方式了。

HTTP ONLY

这个选项和 JavaScript 没有关系,但是我们依旧为了完整性也提一下它。

Web 服务器使用 Set-Cookie header 来设置 cookie。并且,同样的它可以设置 httpOnly 选项。 这个选项禁止任何 JavaScript 访问 cookie。我们使用 document.cookie 看不到此类 cookie,也无法对此类 cookie 进行操作。

这是一种预防措施,当黑客将自己的 JavaScript 代码注入网页,并等待用户访问该页面时发起攻击,而这个选项可以防止此时的这种攻击。这应该是不可能发生的,黑客应该无法将他们的代码注入我们的网站,但是网站有可能存在 bug,使得黑客能够实现这样的操作。

通常来说,如果发生了这种情况,并且用户访问了带有黑客 JavaScript 代码的页面,黑客代码将执行并通过 document.cookie 获取到包含用户身份验证信息的 cookie。但这是我们永远不希望看到的。

但是,如果 cookie 设置了 httpOnly,那么 document.cookie 则看不到 cookie,所以它也就受到了保护。

总结:

整篇读下来我们了解了 Cookie 的前世今生,包括其作用场景以及为什么有些网站可以读取本地的 cookie 来推广广告,同时也知道了在cookie之外的追踪用户行为的多种方式。并且知道关于 Cookie 在生成时就会被指定一个 Expire 或者设定 Max-age 值,这也是 Cookie 的生存周期,在这个周期内 Cookie 有效,超出周期 Cookie 就会被清除。

关于 Cookie 的安全问题,我们从各个角度出发并深度的分析了不同情况下的参数配置问题。

最后: Cookie 数据信息是不会放在客户端的 在极小数的情况下才会这么做 因为它存在一个弊端就是 空间不足。一般来说,我们会把会话的信息,放在服务器上这便是第三方cookie的概念。 Cookie 是直接存储在浏览器中的一小串数据。它们是 HTTP 协议的一部分,由 RFC 6265 规范定义。 对于 Cookie 安全来说,意在希望保护用户在所有场景下的私密安全。