這篇文章基於[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 的現象級微場景編輯器。