面試官的一個簡單問題,卻讓我陷入了深思。這不僅是前端問題,更是全棧工程師必須掌握的安全基礎。
“說說看,使用者登入後拿到的 Token,你會存在哪裡?”
記得我第一次被問到這個問題時,信心滿滿地回答:“localStorage 呀,簡單方便。”然後,空氣突然安靜了...
有後端小夥伴可能會問,這種前端存儲問題後端也需要關心嗎?答案是:絕對需要! 安全是一個全鏈路的問題,任何一環的疏忽都會導致整個系統的崩潰。
很多前端開發者的第一反應都是 localStorage,因為它確實簡單直觀:
// 登入成功後
localStorage.setItem('token', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...');
// 請求時自動攜帶
axios.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
優點很明顯:
但致命問題在於:
這是最經典的解決方案,透過服務端設置 HttpOnly 標誌來保護 Token:
// 服務端設置 Cookie(Node.js/Express 範例)
res.cookie('token', 'eyJhbGci...', {
httpOnly: true, // 禁止 JavaScript 訪問
secure: process.env.NODE_ENV === 'production', // 僅 HTTPS
sameSite: 'strict', // 防禦 CSRF
maxAge: 24 * 60 * 60 * 1000 // 1天有效期
});
前端無需特殊處理:
// 瀏覽器會自動在每次請求中攜帶 Cookie
// 前端 JavaScript 無法讀取,徹底防禦 XSS
配套的 CSRF 防護方案:
// 方案1:CSRF Token
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
axios.defaults.headers.common['X-CSRF-Token'] = csrfToken;
// 方案2:雙重提交 Cookie 驗證
// 服務端同時驗證 Cookie 和 Header 中的 Token
適用場景:
對於安全性要求極高的場景,內存存儲是最安全的選擇:
let memoryToken = null;
// 登入後存儲
const login = async (credentials) => {
const response = await axios.post('/api/login', credentials);
memoryToken = response.data.token;
return response;
};
// 請求攔截器
axios.interceptors.request.use(config => {
if (memoryToken) {
config.headers.Authorization = `Bearer ${memoryToken}`;
}
return config;
});
// 登出或頁面關閉時清理
const logout = () => {
memoryToken = null;
};
優勢:
缺點:
適用場景:
這才是現代 Web 應用在安全與體驗間的完美平衡:
| Token 類型 | 存儲位置 | 有效期 | 用途 |
|---|---|---|---|
| Access Token | 內存 | 短(15分鐘-2小時) | API 呼叫身份驗證 |
| Refresh Token | HttpOnly Cookie | 長(7天-30天) | 刷新 Access Token |
實現方案:
// 登入處理
const handleLogin = async (credentials) => {
const response = await axios.post('/api/login', credentials);
const { accessToken } = response.data;
// Access Token 存內存
setAccessToken(accessToken);
// Refresh Token 由服務端設置為 HttpOnly Cookie
return response;
};
// 請求攔截器 - 自動攜帶 Access Token
axios.interceptors.request.use(config => {
const token = getAccessToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 響應攔截器 - 自動刷新 Token
axios.interceptors.response.use(
response => response,
async error => {
if (error.response?.status === 401) {
// Access Token 過期,嘗試刷新
try {
const newToken = await refreshToken();
setAccessToken(newToken);
// 重試原始請求
error.config.headers.Authorization = `Bearer ${newToken}`;
return axios.request(error.config);
} catch (refreshError) {
// 刷新失敗,跳轉登入頁
logout();
window.location.href = '/login';
}
}
return Promise.reject(error);
}
);
刷新 Token 的服務端實現:
app.post('/api/refresh', async (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) {
return res.status(401).json({ message: '需要刷新 Token' });
}
try {
const decoded = verifyRefreshToken(refreshToken);
const newAccessToken = generateAccessToken({ userId: decoded.userId });
res.json({ accessToken: newAccessToken });
} catch (error) {
res.clearCookie('refreshToken');
res.status(401).json({ message: '無效的刷新 Token' });
}
});
| 存儲方案 | 安全性 | 用戶體驗 | 實現複雜度 | 適用場景 |
|---|---|---|---|---|
| localStorage | ❌ 低 | ✅ 好 | ✅ 簡單 | 內部工具、演示項目 |
| HttpOnly Cookie | ✅ 高 | ✅ 好 | ✅ 中等 | 傳統 Web 應用、SSR |
| 內存存儲 | ✅ 極高 | ❌ 差 | ✅ 簡單 | 高安全要求系統 |
| 雙 Token 機制 | ✅ 很高 | ✅ 好 | ❌ 複雜 | 現代 SPA 應用 |
初級回答:
"localStorage,因為簡單方便。"
中級回答:
"用 HttpOnly Cookie,因為能防 XSS,但要配合 CSRF 防護。"
高級回答:
"要看具體場景。如果是內部低風險系統,localStorage 的簡潔性也有價值。如果是傳統 Web 應用,HttpOnly Cookie + CSRF Token 是久經考驗的方案。如果是現代 SPA,我推薦 Access Token + Refresh Token 的組合,在安全和體驗間取得最佳平衡。同時要考慮業務的安全要求、用戶的使用習慣和技術團隊的維護能力。"
這才是面試官想聽到的:
回頭看我當初那個 naive 的 "localStorage" 回答,問題不在於技術本身,而在於思考方式。
真正的安全專家不是追求絕對安全,而是懂得:
現在當面試官再問我 "Token 該存在哪裡" 時,我會先反問:
"咱們的業務場景是什麼?安全要求級別多高?目標用戶的使用習慣怎樣?技術團隊的維護能力如何?"
因為,沒有最好的方案,只有最合適的方案。安全之路,需要的是持續學習和深度思考。