作者: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接口),瀏覽器會自動帶上我們安全的RefreshTokenCookie,后端驗證通過后,就會返回一個新的AccessToken,我們再把它存入內存。
這個方案完美地結合了安全性和可用性,幾乎無懈可擊。
一張表格說透
| | | |
|---|
| | | |
| | | |
| | | |
| **內存 + `HttpOnly` Cookie** | **安全** (防XSS+CSRF), **體驗好** | | **最佳實踐** (`AccessToken`存內存,`RefreshToken`存`HttpOnly Cookie`) |
希望這篇文章能徹底幫你理清思路。當你在實踐中或者面試被問到時,就可以把這套“方案”發(fā)揮出來。
閱讀原文:原文鏈接
該文章在 2025/8/25 13:08:40 編輯過