摘要:你有没有过这样的经历?项目上线前突然发现 Token 存储出了问题 —— 要么刷新页面后用户直接登出,要么被测试测出 XSS 漏洞风险,紧急修改时还得兼顾兼容性,最后加班到半夜才搞定?
你有没有过这样的经历?项目上线前突然发现 Token 存储出了问题 —— 要么刷新页面后用户直接登出,要么被测试测出 XSS 漏洞风险,紧急修改时还得兼顾兼容性,最后加班到半夜才搞定?
作为前端开发,我们每天和用户认证打交道,而 Token 存储看似是基础操作,却藏着不少容易踩的坑。今天就从实际开发中的问题出发,跟大家聊聊 Token 该怎么存、不同场景下该选哪种方案,帮你避开那些 “上线即翻车” 的坑。
上周帮同事排查一个线上 bug 时,发现他们的项目把 Token 存在了 sessionStorage 里,结果用户打开新标签页登录后,原标签页刷新就提示 “登录已过期”。后来查了才知道,sessionStorage 是会话级存储,每个标签页都是独立的会话,这就导致多标签页场景下 Token 无法共享。
还有一次更惊险的,之前参与的一个电商项目,为了图方便把 Token 存在了 localStorage 里,上线后没几天就被安全团队测出 XSS 漏洞 —— 攻击者通过注入脚本获取了 localStorage 里的 Token,模拟用户登录操作,差点造成用户信息泄露。
这些问题其实不是个例,我在开发者社区做了个小调研,发现超过 60% 的前端开发者都在 Token 存储上踩过坑,其中 “多标签页共享失效”“XSS 攻击风险”“Token 过期处理混乱” 是最常见的三类问题。而这些问题的根源,往往是我们一开始对 Token 存储方案的选型考虑不够全面。
在聊解决方案之前,我们得先明确一个问题:Token 到底是用来干嘛的?简单说,Token 是服务器给客户端的 “身份凭证”,客户端每次发起请求时带上 Token,服务器通过验证 Token 来确认用户身份。所以 Token 的存储,本质上是要解决 “如何安全、稳定地保存这个凭证” 的问题。
目前前端常用的 Token 存储方案主要有三种:localStorage、sessionStorage、Cookie,这三种方案的底层特性完全不同,也决定了它们的适用场景:
localStorage:持久化存储,除非手动清除,否则会一直保存在浏览器中,容量大概 5-10MB。优点是存储时间长、可跨会话访问,缺点是容易受到 XSS 攻击 —— 因为 JavaScript 可以直接读取 localStorage 中的数据,一旦页面被注入恶意脚本,Token 就可能被窃取。sessionStorage:会话级存储,只在当前标签页的会话中有效,关闭标签页后数据就会被清除,容量同样是 5-10MB。优点是安全性比 localStorage 高,因为数据不会共享到其他标签页,缺点是多标签页场景下无法共享 Token,而且刷新页面后如果会话没结束,数据还在,但关闭标签页就没了。Cookie:可设置过期时间的存储,容量只有 4KB 左右。优点是可以通过设置 “HttpOnly” 属性禁止 JavaScript 读取,从根本上避免 XSS 攻击;还能设置 “SameSite” 属性防止 CSRF 攻击;缺点是容量小,而且每次发起请求时都会自动携带 Cookie,会增加请求体积,如果 Cookie 太多,可能会影响接口响应速度。这里要特别提醒大家,很多开发者会忽略 Token 的 “过期机制”——Token 通常会设置过期时间(比如 Access Token 有效期 2 小时,Refresh Token 有效期 7 天),如果存储方案没考虑过期后的刷新逻辑,就会导致用户在操作过程中突然被踢下线,影响用户体验。
知道了底层逻辑和常见问题,接下来就针对不同场景,给出具体的解决方案,每个方案都附了实际项目中用过的代码示例,大家可以直接参考。
如果你的项目是纯 SPA,用户通常只在一个标签页操作(比如管理后台),而且对安全性要求不是特别高(比如内部系统,不是面向外部用户的电商、金融类应用),可以考虑用sessionStorage + Token 过期刷新的方案。
具体实现步骤:
登录成功后,将服务器返回的 Access Token 存入 sessionStorage,Refresh Token 存入 Cookie(设置 HttpOnly、SameSite 属性)。每次发起请求前,从 sessionStorage 中获取 Access Token,放在请求头的 Authorization 字段中。当服务器返回 “Token 过期”(通常是 401 状态码)时,调用刷新 Token 接口,用 Cookie 中的 Refresh Token 获取新的 Access Token,然后更新 sessionStorage 中的 Token,继续发起原请求。代码示例(基于 Axios 拦截器):
// 登录成功后存储Tokenconst handleLogin = async (username, password) => { const res = await axios.post('/api/login', { username, password }); const { accessToken, refreshToken } = res.data; // Access Token存入sessionStorage sessionStorage.setItem('accessToken', accessToken); // Refresh Token存入Cookie,设置HttpOnly、SameSite document.cookie = `refreshToken=${refreshToken}; HttpOnly; SameSite=Strict; Path=/`;};// Axios请求拦截器:添加Tokenaxios.interceptors.Request.use(config => { const accessToken = sessionStorage.getItem('accessToken'); if (accessToken) { config.headers.Authorization = `Bearer ${accessToken}`; } return config;});// Axios响应拦截器:处理Token过期axios.interceptors.response.use( response => response, async error => { const originalRequest = error.config; // 避免重复刷新Token if (error.response.status === 401 && !originalRequest._retry) { originalRequest._retry = true; try { // 调用刷新Token接口 const res = await axios.post('/api/refreshToken'); const { accessToken } = res.data; // 更新sessionStorage中的Token sessionStorage.setItem('accessToken', accessToken); // 重新发起原请求 originalRequest.headers.Authorization = `Bearer ${accessToken}`; return axios(originalRequest); } catch (err) { // 刷新Token失败,跳转到登录页 window.location.href = '/login'; return Promise.reject(err); } } return Promise.reject(error); });如果你的项目是多标签页应用(比如电商网站、社交平台),用户可能会同时打开多个标签页操作,这时候 sessionStorage 的 “不共享” 特性就会出问题,推荐用Cookie + HttpOnly + SameSite的方案。
具体实现步骤:
登录成功后,将 Token 存入 Cookie,同时设置以下关键属性:HttpOnly:禁止 JavaScript 读取,防止 XSS 攻击;SameSite=Strict:防止 CSRF 攻击,只允许同站点的请求携带该 Cookie;Secure:只在 HTTPS 协议下传输(生产环境必须开启);Max-Age:设置 Token 的过期时间,比如 Access Token 设为 7200 秒(2 小时)。浏览器会自动在每次请求中携带 Cookie,服务器通过解析 Cookie 中的 Token 来验证用户身份。Token 过期时,服务器返回 401 状态码,前端跳转到登录页重新登录(如果需要无感知刷新,可以再存一个 Refresh Token 在 Cookie 中,逻辑类似场景 1)。代码示例(后端设置 Cookie,以 Node.js Express 为例):
// 登录接口:设置Token到Cookieapp.post('/api/login', (req, res) => { const { username, password } = req.body; // 验证用户身份(省略逻辑) const accessToken = generateAccessToken(username); // 生成Access Token // 设置Cookie res.cookie('accessToken', accessToken, { httpOnly: true, // 禁止JS读取 sameSite: 'strict', // 防止CSRF secure: process.env.NODE_ENV === 'production', // 生产环境开启HTTPS maxAge: 7200 * 1000, // 2小时过期(毫秒) path: '/' // 所有路径都可访问 }); res.json({ success: true, message: '登录成功' });});// 验证Token的中间件const verifyToken = (req, res, next) => { const accessToken = req.cookies.accessToken; if (!accessToken) { return res.status(401).json({ message: '请先登录' }); } // 验证Token(省略逻辑) next;};// 需要登录的接口app.get('/api/userInfo', verifyToken, (req, res) => { // 返回用户信息 res.json({ username: req.username, role: req.role });});如果你的项目涉及用户资金、敏感信息(比如支付、理财类应用),对安全性要求极高,推荐用Cookie + HttpOnly + SameSite + 双重 Token 验证的方案。
具体实现逻辑:
登录成功后,服务器返回两个 Token:Access Token(短期有效,比如 15 分钟)和 Refresh Token(长期有效,比如 7 天),两个 Token 都存入 Cookie,且都设置 HttpOnly、SameSite、Secure 属性。每次发起请求时,浏览器自动携带两个 Token,服务器先验证 Access Token,如果 Access Token 过期,再验证 Refresh Token;如果 Refresh Token 也过期,就强制用户重新登录。为了防止 Refresh Token 被窃取后长期滥用,可以在服务器端维护一个 “Refresh Token 黑名单”,当用户登出时,将对应的 Refresh Token 加入黑名单,即使 Token 没过期也无法使用。这种方案的优势在于,即使 Access Token 被窃取,因为有效期很短,攻击者能利用的时间窗口非常小;而 Refresh Token 虽然有效期长,但因为存在服务器黑名单,登出后就能立即失效,安全性大大提升。
看到这里,可能有同学会说 “场景太多,记不住怎么办?” 其实不用死记硬背,只要按照这 3 步来,就能快速选出适合自己项目的 Token 存储方案:
第一步:明确项目场景 —— 是单标签页还是多标签页?是否涉及敏感信息?
第二步:优先考虑安全性 —— 如果是高安全性需求,直接选 Cookie + HttpOnly;如果是普通内部系统,sessionStorage 也能用,但要注意 XSS 防护。
第三步:处理边界问题 —— 多标签页要考虑 Token 共享,长期登录要考虑 Token 过期刷新,这些细节一定要在前期就规划好,避免上线后返工。
最后,想跟大家说的是,Token 存储虽然是个小知识点,但却直接关系到项目的安全性和用户体验。如果你在实际开发中遇到了 Token 相关的问题,欢迎在评论区留言,比如 “你项目中用的是哪种存储方案?踩过哪些坑?”,我们一起交流探讨,互相避坑~
来源:从程序员到架构师
