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