面试官的一个简单问题,却让我陷入了深思。这不仅是前端问题,更是全栈工程师必须掌握的 security 基础。
"说说看,用户登录后拿到的 Token,你会存在哪里?"
记得我第一次被问到这个问题时,信心满满地回答:"localStorage 呗,简单方便。"然后,空气突然安静了...
有后端小伙伴可能会问,这种前端存储问题后端也需要关心吗?答案是:绝对需要! 安全是一个全链路问题,任何一环的疏忽都会导致整个系统的崩溃。
初探:天真的 localStorage 方案
很多前端开发者的第一反应都是 localStorage,因为它确实简单直观:
ini
// 登录成功后
localStorage.setItem('token', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...');
// 请求时自动携带
axios.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
优点很明显:
-
简单直观,上手快速
-
持久化存储,页面刷新不影响用户体验
-
API 友好,操作方便
但致命问题在于:
-
一旦遭遇 XSS 攻击,攻击者可以直接通过 JavaScript 读取你的 Token
-
相当于把家门钥匙放在门口的垫子下面
-
几乎无法有效防御 XSS 窃取
深入:真正的解决方案
方案一:HttpOnly Cookie - 传统的智慧
这是最经典的解决方案,通过服务端设置 HttpOnly 标志来保护 Token:
arduino
// 服务端设置 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天有效期
});
前端无需特殊处理:
arduino
// 浏览器会自动在每次请求中携带 Cookie
// 前端 JavaScript 无法读取,彻底防御 XSS
配套的 CSRF 防护方案:
dart
// 方案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
适用场景:
-
传统多页面应用
-
SSR 服务端渲染项目
-
对 SPA 单页应用也完全可行
方案二:内存存储 - 极致的安全追求
对于安全性要求极高的场景,内存存储是最安全的选择:
ini
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;
};
优势:
-
完全不持久化,免疫 XSS 攻击
-
页面关闭即失效,安全性最高
-
实现简单,无需复杂配置
缺点:
-
页面刷新就需要重新登录,用户体验较差
-
移动端应用切换时可能丢失状态
适用场景:
-
银行、金融等高安全要求应用
-
内部管控系统
-
敏感操作的身份验证
方案三:现代 SPA 的黄金标准 - 双 Token 机制
这才是现代 Web 应用在安全与体验间的完美平衡:
Token 类型
存储位置
有效期
用途
Access Token
内存
短(15分钟-2小时)
API 调用身份验证
Refresh Token
HttpOnly Cookie
长(7天-30天)
刷新 Access Token
实现方案:
ini
// 登录处理
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 的服务端实现:
ini
app.post('/api/refresh', async (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) {
return res.status(401).json({ message: 'Refresh token required' });
}
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: 'Invalid refresh 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 该存哪里" 时,我会先反问:
"咱们的业务场景是什么?安全要求等级多高?目标用户的使用习惯怎样?技术团队的维护能力如何?"
因为,没有最好的方案,只有最合适的方案。安全之路,需要的是持续学习和深度思考。