[前端漫談]淺談 Cookie

導讀

這篇文章基於[RFC 6265],簡單說明 Cookie 的使用和特性。大概包括以下四個內容:1)介紹 Cookie 的使用;2)詳解 Cookie 的格式;3)測試 Cookie 在各類狀況下的反應;4)CSRF 攻擊說明html

環境、工具及前置知識

  1. 系統:macOS Mojava
  2. IDE:IDEA
  3. SwitchHosts
  4. OpenSSL
  5. Chrome
  6. js 基礎
  7. NodeJS 基礎
  8. HTTP 基礎

0x001 Cookie 的使用

cookie 的使用很是簡單,能夠概括爲 4 步:前端

  1. 前端發送 HTTP 請求到後端
  2. 後端生成要放到 cookie 中的信息並設置到響應中的 Set-Cookie 頭部
  3. 前端取出響應中的 Set-Cookie 的內容,保存 cookie 信息到本地
  4. 前端繼續訪問後端頁面,將保存到本地的 cookie 放到請求的 Cookie 頭部

用圖說明: java

Cookie 簡單使用

用代碼說明: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

  1. 建立一個 HTTP 服務器
  2. 爲每個響應添加一個 Set-Cookie 頭部,它的值是 name=123
  3. 響應的正文是一個 html,其中只有一個 script 標籤,標籤內的腳本會將 cookie 輸出到當前頁面

啓動這個腳本,而後打開瀏覽器訪問:git

$ node index.js
$ open http://localhost:3000
複製代碼

就能夠看到:github

  1. 頁面上顯示 name=123,正是咱們 Set-Cookie 的內容
  2. HTTPresponse 頭部有 Set-Cookie: name=123

Cookie 簡單使用

打開 Chrome 調試工具的 Application -> cookies -> localhost:3000,就能夠看見咱們設置的 cookie 了:ajax

存儲在本地的 cookies

此時再打開一個 tab,而後再訪問這個頁面(或者直接刷新一下就好,不過爲了對比,能夠再開一個 tab):axios

在 request 中攜帶 cookie

能夠看到,此時,對比第一次訪問的時候,在 request 中多了一個 Cookie,而 Cookie 的內容就是咱們 Set-Cookie 的內容(這不表示 Cookie === Set-Cookie,只是說明 Cookie 的內容來自 Set-Cookie,在後續會有詳細說明 Set-CookieCookie 的轉化)。後端

這就是最簡單的 cookies 使用了。

0x002 cookies 的格式詳解

cookies 的屬性

Chromecookies 管理工具能夠看出一條 cookie 的屬性是有不少的:

  • Name
  • Value
  • Domain
  • Path
  • Expire / Max-Age
  • Size(忽略,應該只是前端統計 Cookie 鍵值對的長度,好比"name"和"123"的長度是 7,若是有大神知道請告知)
  • HttpOnly
  • Secure
  • SameSite

在接下來的章節,將會慢慢解釋這些屬性。

1. Expire

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

帶 epires 的 cookie
能夠看見第一次訪問的時候

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

expires 內的 request

而在 1 分鐘之後,則會從新生成一個 Set-Cookie,而且 request 中的 Cookie 沒了:

從新生成的 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,也就是在當前瀏覽器關閉之後就會刪除。

2. Max-Age

Max-Age 也是用來設置 cookie 的過時時間,可是它設置的是相對於資源獲取的時間的相對秒數。好比,第一次訪問的時候,responseDateSun, 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。

response 中帶 max-age

若是在 60 s 內訪問,則會看到 request 中包含了一個 Cookie: name=123,而 response 中沒有Set-Cookie

max 有效期 request 中帶 cookie

在 60 s 之後訪問,則又建立了新的 cookie。

max-age 的默認值是 session,當前瀏覽器關閉之後就會被刪除。

max-age 的格式是:

max-age-av = "Max-Age=" non-zero-digit *DIGIT
複製代碼

3. Domain

Domain 限制在哪一個域名下會將這個 cookie 包含到 requestCookie 頭部中。

好比,若是咱們設置一個 cookie 的 Domain 屬性是 example.com,則用戶代理會在發往 example.comwww.example.comwww.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
複製代碼

4. Path

相對於 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
複製代碼

5. Secure

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 端口,提供 httphttps 服務,其中,https 服務所需的證書由 openssl 生成,這裏免去不講。訪問 https://example.com,獲得 cookie

刷新頁面,發送 cookie

訪問 http://example.com,獲得 cookie

可是被拒絕:

secure 的默認值是 false,也就是 http/https都能訪問。

secure 的格式是:

secure-av = "Secure"
複製代碼

6. HttpOnly

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"
複製代碼

7. SameSite

注意,在說這個屬性以前,有一點須要說明,那就是請求頭中發送 cookie 並非只有在地址欄輸入這個網址的時候纔會發送這個 cookie,而是在整個瀏覽器的全部頁面內,發送的全部 http 請求,好比 imgsrcscriptsrclinksrciframsrc,甚至 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,由於前面的代碼,因此會返回 scriptimgiframe,而後用戶代理會加載這三個資源:

打開這三個資源,能夠看到,每一個資源都攜帶了一個 cookie

  • script.js

  • img.jpg

  • index.html

SameSite 就是用來限制這種狀況:

Strict

SameSiteStrict 的時候,用戶代理只會發送和網站 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

  • 包括從這個頁面跳轉過去:

  • 子域名

Lax

新版瀏覽器默認的 SameSiteLax,若是 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)

複製代碼

如下是個人測試結果

  • a 連接跳轉:帶 cookie
  • form[action=get]:帶 cookie
  • form[action=post]:不帶 cookie
  • iframe:不帶 cookie
  • script:不帶 cookie
  • img:不帶 cookie

也就是簡單導航跳轉帶,而資源加載不帶。

None

在新版瀏覽器,若是要讓 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)
複製代碼

8. Set-Cookie 的格式

其實 Set-Cookie 的格式是由 cookie 鍵值對和屬性列表構成的,因此能夠用以下表示(不用 ABNF):

Set-Cookie: cookie-pair ";" cookie-av ";" cookie-av....
複製代碼

其中 cookie-pair 就是咱們上面寫的相似 name=123 的格式:

cookie-pair = cookie-name "=" cookie-value
複製代碼

後面能夠跟一系列的屬性,cookie-paircookie-av 之間使用 ; 分割,而 cookie-av 能夠表示爲:

cookie-av = expires-av / max-age-av / domain-av /
                path-av / secure-av / httponly-av /
                extension-av
複製代碼

其中的expires-avmax-age-avdomain-avpath-avsecure-avhttponly-av 就對應前面一張圖的各個屬性,可是有點區別,Chrome 實現了 same-origin屬性,可是在 RFC 6265 並無這個屬性,能夠看做是 extension-av

再作一次對應:

  • Name:cookie-name
  • Value:cookie-value
  • Domain:domain-av
  • Path:path-av
  • Expire / Max-Age :expires-av / max-age-av
  • Size(忽略,應該只是前端統計 Cookie 鍵值對的長度,好比"name"和"123"的長度是 7,若是有大神知道請告知)
  • HttpOnly:httponly-av
  • Secure:secure-av
  • SameSite:extension-av

能夠看到其實 cookie 的格式並非和 Chrome 中的 cookies 一致,它將 cookie-paircookie-av 平鋪開來。

0x003 Cookie 在各類狀況下的反映

1. 每種狀態碼下 Set-Cookie 都會被處理嗎?包括 301404500

根據 RFC 6265,用戶代理可能忽略包含在 100 級別的狀態碼,可是必須處理其餘響應中的 Set-Cookie(包括 400 級別和 500 級別)。

根據測試,除了 100101,還有一個 407(未知爲啥)不會處理 Set-Cookie,甚至 999 都會處理

2. 如何發送多個 cookie 到 Set-Cookie,Cookie 又是如何處理的?

上代碼:

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-EncodingAccept-Language。但問題是,, 能夠做爲 cookie-value 的合法值存在,若是降它做爲分隔符會破壞 cookie 的解析😢,因此就這樣咯。

刷新發送 Cookie,能夠看到,被乖乖摺疊了,而且重複的 cookie-namecookie 被按順序覆蓋了:

3. ajax 如何發送 Cookie,ajax 返回的 Set-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才行,XMLHttpRequestFetch 配置可能有所不一樣。

暫時想不到了

0x004 CSRF

例子

  • 打開登錄頁面 localhost:3000/login,登錄以後,後端返回一個 Set-Cookie: name=bob,後端讀取 Cookie 判斷用戶是否登錄,若是登錄,就會顯示帳戶金額,和一個轉帳表單

  • 轉帳是經過 http://localhost:3000/transfer?name=lucy&money=1000 來轉帳的,name 是轉帳目標用戶,money 是金額。這裏爲了方便,直接使用 GET

  • 從新開一個服務,僞裝是另外一個站點,這個站點只有一個 img,該 imgsrc 指向了上面的轉帳地址:
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:3000cookie,因此會被帶過去,而這個地址根據 cookie 判斷用戶,並執行轉帳操做:

  • 若是再 bob 回到本身的帳戶頁面,就會發現,他已然破產,被限制高消費:

防護

(本文章主要講 cookiecsrf 原理和防護不是重點)

  • 檢測 Referrer 頭:可是一些瀏覽器能夠禁用這個頭部(我也沒找到哪一個瀏覽器能夠設置)
  • SameSite:還沒有徹底普及
  • csrftoken:通常 csrf 發起攻擊都是相似上面,搭建一個釣魚網站,其實該釣魚網站是沒法訪問源站的 cookie 的,發送 cookie 是瀏覽器自帶的行爲,因此只要生成一個隨機的 token,在每一個表單發送的時候都帶上就好了,由於釣魚網站沒法預測這個 token 的值。甚至能夠將這個 token 直接存儲在 cookie 中,在表單發送的時候取出來和表單一塊兒發送。也能夠後端生成表單的時候注入到一個 inputp[typehidden] 域。
  • 二次驗證,好比驗證碼、支付密碼等

0x005 資源

0x006 帶貨

最近發現一個好玩的庫,做者是個大佬啊--基於 React 的現象級微場景編輯器

相關文章
相關標籤/搜索