作者:ErpanOmer
https://juejin.cn/post/7521936882353471526
如果你做過任何需要登錄的功能,那么你一定思考過這個問題:當(dāng)后端甩給我一個token
時,我一個前端,到底應(yīng)該把它放在哪兒?
這個問題看似簡單,無非就是 LocalStorage
、SessionStorage
、Cookie
三個選項(xiàng)。但如果我告訴你,一個錯誤的選擇,可能會直接導(dǎo)致你的網(wǎng)站出現(xiàn)嚴(yán)重的安全漏洞,你是不是會驚出一身冷汗?
許多開發(fā)者(包括曾經(jīng)的我)不假思索地把token
塞進(jìn)LocalStorage
,因?yàn)樗腁PI最簡單好用。但這種方便的背后,隱藏著巨大的風(fēng)險。
今天,這篇文章將帶你徹底終結(jié)這個糾結(jié)。我們將深入對比這三位“候選人”的優(yōu)劣,剖析它們各自面臨的安全威脅(XSS 和 CSRF),并最終給出一個當(dāng)前業(yè)界公認(rèn)的最佳實(shí)踐方案。
1. 三種存儲方案對比
在做決定前,我們先來快速了解一下這三個Web存儲方案的基本特性。
| | | |
---|
| | 頁面會話期間(標(biāo)簽頁關(guān)閉即失效) | |
| | | |
| | | |
| | | |
一目了然,LocalStorage
和SessionStorage
是HTML5提供的新API,更大、更易用。而Cookie
是“老前輩”,小而精,并且有個獨(dú)一無二的特性:會自動“粘”在HTTP請求頭里發(fā)給后端。
2. 兩大安全攻擊 XSS 與 CSRF
選擇存儲方案,本質(zhì)上是在權(quán)衡安全和便利。而威脅token
安全的主要是下面兩種。
XSS (跨站腳本攻擊)
- 手法:攻擊者通過某種方式(比如評論區(qū))向你的網(wǎng)站注入了惡意的JavaScript腳本。當(dāng)其他用戶訪問這個頁面時,這段腳本就會執(zhí)行。
- 目標(biāo):如果你的
token
存在LocalStorage
或SessionStorage
里,那么這段惡意腳本就可以通過簡單的localStorage.getItem('token')
輕松地把它偷走,然后發(fā)送到攻擊者的服務(wù)器。token
失竊,你的賬戶就被冒充了。
結(jié)論一:LocalStorage
和 SessionStorage
對 XSS 攻擊是完全不設(shè)防的。只要你的網(wǎng)站存在XSS漏洞,存在里面的任何數(shù)據(jù)都能被輕易竊取。
CSRF (跨站請求偽造)
- 手法:你剛剛登錄了你的銀行網(wǎng)站
bank.com
,你的登錄憑證(Cookie
)被瀏覽器記住了。然后,你沒有關(guān)閉銀行頁面,而是點(diǎn)開了一個惡意網(wǎng)站hacker.com
。這個惡意網(wǎng)站的頁面里可能有一個看不見的表單或<img>
標(biāo)簽,它會自動向bank.com/transfer
這個地址發(fā)起一個轉(zhuǎn)賬請求。 - 目標(biāo):因?yàn)闉g覽器在發(fā)送請求到
bank.com
時,會自動帶上bank.com
的Cookie
,所以銀行服務(wù)器會認(rèn)為這個請求是你本人發(fā)起的,于是轉(zhuǎn)賬就成功了。你神不知鬼不覺地被“偽造”了意愿。
結(jié)論二:Cookie
如果不加以保護(hù),會受到 CSRF 攻擊的威脅。
3. 現(xiàn)代Cookie的“優(yōu)勢”
看到這里你可能會想:LocalStorage
防不住XSS,Cookie
防不住CSRF,這可怎么辦?
別急,我們的Cookie
經(jīng)過多年的進(jìn)化,已經(jīng)有了強(qiáng)大的防止手段。
HttpOnly
- 封印JS的訪問
如果在設(shè)置Cookie
時,加上HttpOnly
屬性,那么通過JavaScript(如 document.cookie
)將無法讀取到這個Cookie
。
Set-Cookie: token=...; HttpOnly
這意味著,即使網(wǎng)站存在XSS漏洞,攻擊者的惡意腳本也偷不走這個Cookie
,從根本上阻斷了XSS利用token
的路徑。
SameSite
- 防止攜帶
SameSite
屬性用來告訴瀏覽器,在跨站請求時,是否應(yīng)該攜帶這個Cookie
。它有三個值:
Strict
:最嚴(yán)格。只有當(dāng)請求的發(fā)起方和目標(biāo)網(wǎng)站完全一致時,才會攜帶Cookie
,能完全防御CSRF。Lax
:比較寬松(現(xiàn)在是大多數(shù)瀏覽器的默認(rèn)值)。允許在“頂級導(dǎo)航”(如<a>
鏈接、GET表單)的跨站請求中攜帶Cookie
,但在<img>
、<iframe>
、POST表單等“嵌入式”請求中會攔截。這已經(jīng)能防御大部分CSRF攻擊了。None
:最松。任何情況下都攜帶Cookie
。但必須同時指定Secure
屬性(即Cookie
只能通過HTTPS發(fā)送)。
對于登錄token
,我們通常希望它盡可能安全,所以SameSite=Strict
是最佳選擇。
Secure
- 保證傳輸安全
這個屬性很簡單,只要設(shè)置了它,Cookie
就只會在HTTPS的加密連接中被發(fā)送,可以防止在傳輸過程中被竊聽。
4. 終極答案
綜合以上所有分析,我們終于可以給出當(dāng)前公認(rèn)的最佳、最安全的方案了。
這個方案的核心是“組合拳”:將不同生命周期的token
存放在不同的地方,各司其職。
我們通常有兩種token
:
- **
AccessToken
**:生命周期很短(如15分鐘),用于訪問受保護(hù)的API資源。 - **
RefreshToken
**:生命周期很長(如7天),專門用來在AccessToken
過期后,換取一個新的AccessToken
。
最佳存儲策略如下:
RefreshToken
: 存放在一個 HttpOnly=true
, Secure=true
, SameSite=Strict
的Cookie
中。
* **為什么?** `RefreshToken`非常關(guān)鍵且長期有效,所以必須用最安全的方式存儲。`HttpOnly`讓它免受XSS攻擊,`SameSite=Strict`讓它免受CSRF攻擊。前端 JS 完全接觸不到它,只在需要刷新`token`時,由瀏覽器自動帶著它去請求`/refresh_token`這個特定接口。
AccessToken
: 存放在 JavaScript的內(nèi)存中(例如,一個全局變量、React Context或Vuex/Pinia等狀態(tài)管理庫里)。
* **為什么?** `AccessToken`需要被JS讀取,并放在HTTP請求的`Authorization`頭里(`Bearer xxx`)發(fā)送給后端。將它放在內(nèi)存中,可以避免XSS直接從`LocalStorage`里掃蕩。當(dāng)用戶關(guān)閉標(biāo)簽頁或刷新頁面時,內(nèi)存中的`AccessToken`會丟失。
- 丟失了怎么辦? 這就是
RefreshToken
發(fā)揮作用的時候了。當(dāng)應(yīng)用啟動或AccessToken
失效時,我們就向后端發(fā)起一個請求(比如訪問/refresh_token
接口),瀏覽器會自動帶上我們安全的RefreshToken
Cookie
,后端驗(yàn)證通過后,就會返回一個新的AccessToken
,我們再把它存入內(nèi)存。
這個方案完美地結(jié)合了安全性和可用性,幾乎無懈可擊。
一張表格說透
| | | |
---|
| | | |
| API簡單,標(biāo)簽頁關(guān)閉即刪 | | |
| | | |
**內(nèi)存 + `HttpOnly` Cookie** | **安全** (防XSS+CSRF), **體驗(yàn)好** | | **最佳實(shí)踐** (`AccessToken`存內(nèi)存,`RefreshToken`存`HttpOnly Cookie`) |
希望這篇文章能徹底幫你理清思路。當(dāng)你在實(shí)踐中或者面試被問到時,就可以把這套“方案”發(fā)揮出來。
閱讀原文:原文鏈接
該文章在 2025/7/29 12:35:11 編輯過