这篇文章基于[RFC 6265],简单说明 Cookie 的使用和特性。大概包括以下四个内容:1)介绍 Cookie 的使用;2)详解 Cookie 的格式;3)测试 Cookie 在各类状况下的反应;4)CSRF 攻击说明html
macOS Mojava
IDEA
SwitchHosts
OpenSSL
Chrome
js
基础NodeJS
基础HTTP
基础cookie
的使用很是简单,能够概括为 4 步:前端
HTTP
请求到后端cookie
中的信息并设置到响应中的 Set-Cookie
头部Set-Cookie
的内容,保存 cookie
信息到本地cookie
放到请求的 Cookie
头部用图说明: java
用代码说明:node
const http = require('http');
http.createServer((req, res) => {
res.writeHead(200, {'Set-Cookie': 'name=123'})
res.write(`
<script>document.write(document.cookie)</script>
`)
res.end()
}).listen(3000)
复制代码
这段代码的功能很简单:ios
HTTP
服务器Set-Cookie
头部,它的值是 name=123
html
,其中只有一个 script
标签,标签内的脚本会将 cookie
输出到当前页面启动这个脚本,而后打开浏览器访问:git
$ node index.js
$ open http://localhost:3000
复制代码
就能够看到:github
name=123
,正是咱们 Set-Cookie
的内容HTTP
的 response
头部有 Set-Cookie: name=123
打开 Chrome
调试工具的 Application -> cookies -> localhost:3000
,就能够看见咱们设置的 cookie
了:ajax
此时再打开一个 tab
,而后再访问这个页面(或者直接刷新一下就好,不过为了对比,能够再开一个 tab
):axios
能够看到,此时,对比第一次访问的时候,在 request
中多了一个 Cookie
,而 Cookie
的内容就是咱们 Set-Cookie
的内容(这不表示 Cookie === Set-Cookie
,只是说明 Cookie
的内容来自 Set-Cookie
,在后续会有详细说明 Set-Cookie
到 Cookie
的转化)。后端
这就是最简单的 cookies
使用了。
从 Chrome
的 cookies
管理工具能够看出一条 cookie
的属性是有不少的:
Name
Value
Domain
Path
Expire / Max-Age
Size
(忽略,应该只是前端统计 Cookie 键值对的长度,好比"name"和"123"的长度是 7,若是有大神知道请告知)HttpOnly
Secure
SameSite
在接下来的章节,将会慢慢解释这些属性。
Expires
是用来为一个 cookie
设置过时时间,它是一个 UTC
格式的时间,过了这个时间之后,这个 cookie 就失效了,浏览器不会将这条失效的 cookie
包含在 Cookie
请求头部中,而且会删除这条过时的 cookie
。
代码:
const http = require('http');
http.createServer((req, res) => {
const cookie =req.headers['cookie']
const date = new Date();
date.setMinutes(date.getMinutes()+1)
if (!cookie || !cookie.length){
res.writeHead(200, {'Set-Cookie': `name=123; expires=${date.toUTCString()}`})
}
res.write(`
<script>document.write(document.cookie)</script>
`)
res.end()
}).listen(3000)
复制代码
上面的代码在给 response
添加的 Set-Cookie
中多了一个 expires
属性,就是用来设置过时时间,这里将它设置为一分钟之后:
const date = new Date();
date.setMinutes(date.getMinutes()+1)
复制代码
同时作了一个判断,若是 request
中有 cookie
了,就不添加 Set-Cookie
头部,不然永远不会过时。
打开浏览器(先删除以前的 cookies
),访问localhost:3000
:
Date: Sun, 01 Dec 2019 15:24:47 GMT
Set-Cookie: name=123; expires=Sun, 01 Dec 2019 15:25:47 GMT
复制代码
Set-Cookie 的格式变了,多了一个 expires
属性,而且时间是 Date
属性的 1分钟之后。在这一分钟以内,若是咱们刷新页面,会发现 request
中有 Cookie
,而 response
中没有 Set-Cookie
:
而在 1 分钟之后,则会从新生成一个 Set-Cookie
,而且 request
中的 Cookie
没了:
expires
属性的的格式是:
expires-av = "Expires=" sane-cookie-date
sane-cookie-date = <rfc1123-date, 定义在 [RFC2616], 章节 3.3.1>
复制代码
sane-cookie-date
是一个时间格式,大概以下:
Sun, 06 Nov 1994 08:49:37 GMT
复制代码
js
可使用以下得到:
$ new Date().toUTCString()
"Sun, 01 Dec 2019 15:18:12 GMT"
复制代码
expires
的默认值是 session
,也就是在当前浏览器关闭之后就会删除。
Max-Age
也是用来设置 cookie
的过时时间,可是它设置的是相对于资源获取的时间的相对秒数。好比,第一次访问的时候,response
的 Date
是 Sun, 01 Dec 2019 15:44:43 GMT
,那么这条 cookie
的过时时间就是 Sun, 01 Dec 2019 15:45:43 GMT
第一次请求的时候,能够看到,cookie
的格式是:Set-Cookie: name=123; max-age=60
,多了一个 max-age=60。
若是在 60 s 内访问,则会看到 request
中包含了一个 Cookie: name=123
,而 response
中没有Set-Cookie
在 60 s 之后访问,则又建立了新的 cookie。
max-age
的默认值是 session
,当前浏览器关闭之后就会被删除。
max-age
的格式是:
max-age-av = "Max-Age=" non-zero-digit *DIGIT
复制代码
Domain
限制在哪一个域名下会将这个 cookie 包含到 request
的 Cookie
头部中。
好比,若是咱们设置一个 cookie 的 Domain 属性是 example.com
,则用户代理会在发往 example.com
,www.example.com
,www.corp.example.com
的请求的 Cookie
中携带这个 cookie
。
上代码:
const http = require('http');
http.createServer((req, res) => {
const cookie =req.headers['cookie']
const date = new Date();
date.setMinutes(date.getMinutes()+1)
if (!cookie || !cookie.length){
res.writeHead(200, {'Set-Cookie': 'name=123;domain=example.com'})
}
res.write(`
<script>document.write(document.cookie)</script>
`)
res.end()
}).listen(3000)
复制代码
这里咱们添加了一个 domain=example.com
。而后使用 SwitchHost
配置几个 host
:
127.0.0.1 example.com
127.0.0.1 www.example.com
127.0.0.1 www.corp.example.com
复制代码
访问 example.com:3000
,第一次访问的时候,会返回
Set-Cookie: name=123;domain=example.com
复制代码
而后咱们刷新页面,就会发现 cookie
被携带到 request
中:
访问 www.example.com:3000
,也存在这个 cookie
访问 www.corp.example.com:3000
,也存在这个 cookie
可是若是访问 exampel2.com:3000
,就不存在了,而且,由于 cookie
中的 domain
和当前的 domain
不一样,用户代理会拒绝存储这个 cookie
,因此咱们看不到 cookie
的输出:
cookie
管理中,也不存在这条
cookie
:
domain
的默认值是当前的域名。
domain
的格式是:
domain-av = "Domain=" domain-value
复制代码
相对于 Domain
限于 cookie
的域名,Path
限制 cookie
的路径,好比,若是咱们设置 cookie
的路径是 /a
,则在 /
或者 /b
就没法使用
上代码:
const http = require('http');
http.createServer((req, res) => {
const cookie =req.headers['cookie']
const date = new Date();
date.setMinutes(date.getMinutes()+1)
if (!cookie || !cookie.length){
res.writeHead(200, {'Set-Cookie': 'name=123;path=/a'})
}
res.write(`
<script>document.write(document.cookie)</script>
`)
res.end()
}).listen(3000)
复制代码
这里添加了一个 path=/a
,其余就没有变化了,访问 localhost:3000/a
,获得一个 cookie
:
刷新页面,发送刚刚获得的 cookie
:
访问 /a/b
,却能够:
访问 /b
,不会发送 cookie
,而且会生成一个新的 cookie
:
可是因为和当前路径不匹配,被拒绝:
访问 /
,则和访问 /b
同样的结果:
所以,Path 能够限制一个 cookie
在哪一个路径及其子路径下可用,祖先路径和兄弟路径则不在容许之列,若是没有这个值,则是 /
Path
的默认值是 /
,也就是对整个站是有效的。
path
的格式是:
path-av = "Path=" path-value
复制代码
Secure
用来指示一个 cookie
只在“安全”通道发送,这里的安全通道,通常指的就是 https
。意思就是只有在 https
的时候才发送,http
不发送。
上代码:
let https = require("https");
let http = require("http");
let fs = require("fs");
const options = {
key: fs.readFileSync('./server.key'),
cert: fs.readFileSync('./server.pem')
};
const app = (req, res) => {
const cookie =req.headers['cookie']
const date = new Date();
date.setMinutes(date.getMinutes()+1)
if (!cookie || !cookie.length){
res.writeHead(200, {'Set-Cookie': 'name=123;secure'})
}
res.write(`
<script>document.write(document.cookie)</script>
`)
res.end()
}
https.createServer(options, app).listen(443);
http.createServer(app).listen(80);
复制代码
这里同时兼通 80、443
端口,提供 http
和 https
服务,其中,https
服务所需的证书由 openssl
生成,这里免去不讲。访问 https://example.com
,获得 cookie
:
刷新页面,发送 cookie
:
访问 http://example.com
,获得 cookie
:
可是被拒绝:
secure
的默认值是 false
,也就是 http/https
都能访问。
secure
的格式是:
secure-av = "Secure"
复制代码
Secure
限制 cookie
只能在安全通道中使用,别觉得 HttpOnly
就是只能在 Http
中使用的意思,HttpOnly
的意思是只能经过 http
才能访问,在前面的例子中,咱们一直经过document.cookie
来在前端访问 cookie
,可是若是指定了这个,就没法经过document.cookie
来操做 cookie
了。
上代码:
const http = require('http');
http.createServer((req, res) => {
const cookie =req.headers['cookie']
const date = new Date();
date.setMinutes(date.getMinutes()+1)
if (!cookie || !cookie.length){
res.writeHead(200, {'Set-Cookie': 'name=123;httpOnly'})
}
res.write(`
<script>document.write(document.cookie)</script>
`)
res.end()
}).listen(3000)
复制代码
多了一个 httpOnly
,访问 localhost:3000
,获得 cookie
:
刷新,发送 cookie
:
使用 document.cookie
操做 cookie
:
> document.cookie = 'name=bar'
< "name=bar"
> document.cookie
< ""
复制代码
而后查看 cookie
,能够看见 value
依旧是 123
,而 HttpOnly
则被打勾,无情。
httpOnly
的默认值是 fasle
,也就是能够用document.cookie
来操做 cookie
httpOnly
的格式是
httponly-av = "HttpOnly"
复制代码
注意,在说这个属性以前,有一点须要说明,那就是请求头中发送 cookie
并非只有在地址栏输入这个网址的时候才会发送这个 cookie
,而是在整个浏览器的全部页面内,发送的全部 http
请求,好比 img
的 src
,script
的 src
,link
的 src
,ifram
的 src
,甚至 ajax
配置以后,只要知足 上面提到的条件,这个 http
请求都会包含这个请求地址的 cookie
,就算你是在其余网站访问也是同样,这也就是后面讲的 csrf
有机可趁的缘由。
上代码:
const http = require('http');
http.createServer((req, res) => {
if (req.headers.host.startsWith('example')) {
res.writeHead(200, {'Set-Cookie': `host=${req.headers.host}`})
}
if (req.headers.host.startsWith('localhost')) {
res.write(`
<script src="http://example.com:3000/script.js"></script>
<img src="http://example.com:3000/img.jpg"/>
<iframe src="http://example.com:3000/"/>
`)
}
res.end()
}).listen(3000)
复制代码
若是访问的地址是 example
开头,设置了一个 cookie
,名字为 host
,值为当前访问的地址。 若是访问的地址是localhost
开头,就返回 3 个元素,他们的 src
指向 example.com:3000
三个不一样的资源(尽管不存在,可是足够了)。 访问 example.com:3000
,获得 cookie:host=example.com:3000
刷新页面,正常发送 cookie
:
此时访问 localhost:3000
,由于前面的代码,因此会返回 script
、img
、iframe
,而后用户代理会加载这三个资源:
打开这三个资源,能够看到,每一个资源都携带了一个 cookie
。
script.js
img.jpg
index.html
而 SameSite
就是用来限制这种状况:
当 SameSite
为 Strict
的时候,用户代理只会发送和网站 URL
彻底一致的 cookie
,就算是子域名都不行。
上代码:
const http = require('http');
http.createServer((req, res) => {
if (req.headers.host.startsWith('example')) {
res.writeHead(200, {'Set-Cookie': `host=${req.headers.host};SameSite=Strict`})
}
if (req.headers.host.startsWith('localhost')) {
res.write(`
<a href="http://example.com:3000/index.html">http://example.com:3000/index.html</a>
<script src="http://example.com:3000/script.js"></script>
<img src="http://example.com:3000/img.jpg"/>
<iframe src="http://example.com:3000/index.html"/>
`)
}
res.end()
}).listen(3000)
复制代码
访问 example.com:3000
得到 cookie
:
刷新正常发送 cookie
:
访问 localhost:3000
任何指向 example.com:3000
的资源都不会发送 cookie
:
包括从这个页面跳转过去:
子域名
新版浏览器默认的 SameSite
是 Lax
,若是 SameSite
是 Lax,则将会为一些跨站子请求保留,如图片加载或者 frames
的调用,但只有当用户从外部站点导航到URL时才会发送。如 link
连接。(我没能测试出来,具体看阮一峰-Cookie 的 SameSite 属性和MDN-HTTP cookies)
上代码:
const http = require('http');
http.createServer((req, res) => {
if (req.headers.host.startsWith('example')) {
res.writeHead(200, {'Set-Cookie': `host=${req.headers.host};SameSite=Lax`})
}
if (req.headers.host.startsWith('localhost')) {
res.write(`
<a href="http://example.com:3000/index.html">http://example.com:3000/index.html</a>
<form action="http://example.com:3000/form" method="post">
<button>post</button>
</form>
<form action="http://example.com:3000/form" method="get">
<button>get</button>
</form>
<script src="http://example.com:3000/script.js"></script>
<img src="http://example.com:3000/img.jpg"/>
<iframe src="http://example.com:3000/index.html"/>
`)
}
res.end()
}).listen(3000)
复制代码
如下是个人测试结果
也就是简单导航跳转带,而资源加载不带。
在新版浏览器,若是要让 cookie
支持同站和跨站,须要明确指定 SameSite=None
(我测试出来的结果是和没有添加这个属性是一致的)。
上代码:
const http = require('http');
http.createServer((req, res) => {
if (req.headers.host.startsWith('example')) {
res.writeHead(200, {'Set-Cookie': `host=${req.headers.host};SameSite=None`})
}
if (req.headers.host.startsWith('localhost')) {
res.write(`
<a href="http://example.com:3000/index.html">http://example.com:3000/index.html</a>
<form action="http://example.com:3000/form" method="post">
<button>post</button>
</form>
<form action="http://example.com:3000/form" method="get">
<button>get</button>
</form>
<script src="http://example.com:3000/script.js"></script>
<img src="http://example.com:3000/img.jpg"/>
<iframe src="http://example.com:3000/index.html"/>
`)
}
res.end()
}).listen(3000)
复制代码
其实 Set-Cookie
的格式是由 cookie
键值对和属性列表构成的,因此能够用以下表示(不用 ABNF):
Set-Cookie: cookie-pair ";" cookie-av ";" cookie-av....
复制代码
其中 cookie-pair
就是咱们上面写的相似 name=123
的格式:
cookie-pair = cookie-name "=" cookie-value
复制代码
后面能够跟一系列的属性,cookie-pair
和 cookie-av
之间使用 ;
分割,而 cookie-av
能够表示为:
cookie-av = expires-av / max-age-av / domain-av /
path-av / secure-av / httponly-av /
extension-av
复制代码
其中的expires-av
、max-age-av
、domain-av
、path-av
、secure-av
、httponly-av
就对应前面一张图的各个属性,可是有点区别,Chrome
实现了 same-origin
属性,可是在 RFC 6265
并无这个属性,能够看做是 extension-av
,
再作一次对应:
能够看到其实 cookie 的格式并非和 Chrome
中的 cookies
一致,它将 cookie-pair
和 cookie-av
平铺开来。
Cookie
在各类状况下的反映Set-Cookie
都会被处理吗?包括 301
、404
、500
?根据 RFC 6265
,用户代理可能忽略包含在 100
级别的状态码,可是必须处理其余响应中的 Set-Cookie
(包括 400
级别和 500
级别)。
根据测试,除了 100
,101
,还有一个 407
(未知为啥)不会处理 Set-Cookie
,甚至 999 都会处理
上代码:
const http = require('http');
http.createServer((req, res) => {
res.setHeader('Set-Cookie', ['cookie1=1','cookie1=2','cookie2=2','cookie3=3'])
res.end()
}).listen(3000)
复制代码
访问 localhost:3000
,获得 cookie
:
能够看到,cookie
被放到多个 Set-Cookie
头部中,而在 RFC 7230
中其实规定,一个报文中不该该出现重复的头部,若是一个头部有多个值,应该使用 , 分隔,就像下面的Accept-Encoding
和 Accept-Language
。但问题是,,
能够做为 cookie-value
的合法值存在,若是降它做为分隔符会破坏 cookie
的解析😢,因此就这样咯。
刷新发送 Cookie
,能够看到,被乖乖折叠了,而且重复的 cookie-name
的 cookie
被按顺序覆盖了:
答案是会,这里以 axios
为例子,上代码:
const http = require('http');
http.createServer((req, res) => {
res.writeHead(200, {'Set-Cookie': 'name=ajax'})
res.write(`
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script >
axios.get('http://example.com:3000/', {
withCredentials: true
})
</script>
`)
res.end()
}).listen(3000)
复制代码
访问 localhost:3000
,获得 cookie
:
查看 cookie
存储,已经存上了:
查看 ajax
请求,已经发送了:
这里遵循同源策略,若是是同源,则默认会发送,若是是跨域,则须要添加withCredentials: true
才行,XMLHttpRequest
和 Fetch
配置可能有所不一样。
localhost:3000/login
,登录以后,后端返回一个 Set-Cookie
: name=bob
,后端读取 Cookie
判断用户是否登录,若是登录,就会显示帐户金额,和一个转帐表单http://localhost:3000/transfer?name=lucy&money=1000
来转帐的,name
是转帐目标用户,money
是金额。这里为了方便,直接使用 GET
。img
,该 img
的 src
指向了上面的转帐地址:const http = require('http');
http.createServer((req, res) => {
res.writeHead(200, {'Content-Type':'text/html'})
res.write(`
<img src="http://localhost:3000/transfer?name=bad&money=10000000">
`)
res.end()
}).listen(3001)
复制代码
localhost:3000
登录的 bob
访问 example.com:3001
,就会发现,虽然只是一个图片指向了 localhost:3000
,可是由于可以获取到 localhost:3000
的 cookie
,因此会被带过去,而这个地址根据 cookie
判断用户,并执行转帐操做:(本文章主要讲 cookie
,csrf
原理和防护不是重点)
Referrer
头:可是一些浏览器能够禁用这个头部(我也没找到哪一个浏览器能够设置)SameSite
:还没有彻底普及csrftoken
:通常 csrf
发起攻击都是相似上面,搭建一个钓鱼网站,其实该钓鱼网站是没法访问源站的 cookie
的,发送 cookie
是浏览器自带的行为,因此只要生成一个随机的 token
,在每一个表单发送的时候都带上就好了,由于钓鱼网站没法预测这个 token
的值。甚至能够将这个 token
直接存储在 cookie
中,在表单发送的时候取出来和表单一块儿发送。也能够后端生成表单的时候注入到一个 inputp[typehidden]
域。最近发现一个好玩的库,做者是个大佬啊--基于 React 的现象级微场景编辑器。