本地存储与鉴权
面试一句话总结:前端本地存储主要包括 Cookie(用于状态维持,4KB)、Web Storage(
localStorage/sessionStorage,5MB,纯前端使用)和 IndexedDB(本地数据库,存大量结构化数据)。在鉴权场景中,现代应用多使用 JWT,围绕 Token 的存储位置(防 XSS/CSRF)和无感刷新是高频考点。
1. Cookie (HTTP 状态维护者)
Cookie 最初的设计目的不是为了“存储数据”,而是为了让无状态的 HTTP 协议维持会话状态。大小严格限制为 4KB。 每次发同源请求时,浏览器会自动把 Cookie 带在请求头中发给服务端。
核心属性配置(安全与作用域):
Max-Age/Expires:控制存活时间。不设置就是“会话 Cookie”(关浏览器即丢)。Domain/Path:控制 Cookie 作用的域名和路径。子域名共享需显式设置Domain=.example.com。HttpOnly:(防 XSS 必备) 设置为 true 后,前端 JS 无法通过document.cookie读取它。Secure:只能在 HTTPS 环境下传输。SameSite:(防 CSRF 必备) 控制跨站请求是否携带 Cookie。Strict:跨站绝对不带。Lax:默认值。只有顶级导航(如链接跳转)且是 GET 请求时才带。None:跨站也带,但前提是必须同时开启Secure。
2. Web Storage (纯前端存储)
专门为前端存储数据而生,大小一般为 5MB,只支持同步读取,且只存字符串(存对象需 JSON.stringify)。绝对不会自动随 HTTP 发送给服务器。
localStorage (持久存储)
- 生命周期:永久,除非代码清除或用户手动清缓存。
- 作用域:受同源策略限制。同一个浏览器下,同源的多个 Tab 页共享同一个
localStorage。
sessionStorage (会话存储 - 面试易错点)
- 生命周期:仅在当前 Tab 页存活。关闭 Tab 页即销毁。
- 作用域:除了同源限制,它还被严格限制在当前的 Tab 窗口。
面试高频陷阱:
- 刷新页面会清空 sessionStorage 吗? 不会,恢复或强制刷新都会保留。
- 同源的两个 Tab 页共享 sessionStorage 吗? 绝对不共享!
- 从 A 页面通过
<a target="_blank">跳转到同源 B 页面,B 会有 A 的数据吗? 会复制一份(相当于快照),但之后两者独立,互不影响。
3. IndexedDB (前端本地数据库)
当存储需求大于 5MB,或者需要存储二进制文件(Blob、File)时使用。
- 特点:NoSQL 键值对数据库;全异步操作(不阻塞主线程);支持事务;空间极大(硬盘可用空间的 50%)。
4. Token 鉴权与安全存储 (核心考点)
在现代前后端分离架构中,JWT (JSON Web Token) 是最主流的鉴权方案。
面试高频题:Token 存在哪里最安全?
方案 A:存在 localStorage
- 优点:天然防 CSRF(因为不会自动发给服务器,需 JS 手动塞进 Header)。
- 致命缺点:极易受 XSS 攻击。一旦页面被注入恶意脚本,黑客直接
localStorage.getItem('token')拿走。
方案 B:存在 Cookie 中 (并开启 HttpOnly)
- 优点:完美防 XSS(JS 读不到)。
- 致命缺点:极易受 CSRF 攻击。诱导用户点击钓鱼网站的请求,浏览器会自动带上 Cookie 导致被盗用身份。
- 补救措施:设置
SameSite: Lax。
最佳实践(总结话术): 如果业务只在自己公司的 App/浏览器内,用
localStorage配合严密的 XSS 防御(如 Vue/React 自带转义)是最方便的。如果对安全性要求极高(如银行),应采用 Cookie (HttpOnly + Secure + SameSite=Lax),并在请求头中额外附加 CSRF Token 进行双重校验。
5. Token 无感刷新 (双 Token 机制)
为了安全,业务 Token (access_token) 的有效期通常很短(如 30 分钟),而用来换取新 Token 的 refresh_token 有效期很长(如 7 天)。
核心难点:并发请求导致重复刷新
当 access_token 过期时,如果页面同时发出了 3 个接口请求,如何保证只发一次刷新请求,并且这 3 个接口都能无缝重试?
实现代码骨架(基于 Axios 拦截器):
let isRefreshing = false; // 刷新锁
let requestQueue = []; // 暂存因为 token 过期而挂起的请求
axios.interceptors.response.use(undefined, async (error) => {
const { config, response } = error;
// 1. 判断是 401 Token 过期
if (response.status === 401 && !config._retry) {
if (!isRefreshing) {
isRefreshing = true;
try {
// 2. 发起刷新 Token 请求
const newToken = await refreshTokenApi();
// 3. 刷新成功,清空队列并执行
requestQueue.forEach((cb) => cb(newToken));
requestQueue = [];
return axios(config); // 重试当前请求
} catch (e) {
// 刷新也失败了,直接踢回登录页
return Promise.reject(e);
} finally {
isRefreshing = false;
}
} else {
// 4. 如果正在刷新中,就把当前请求挂起(放入队列),等待新 Token
return new Promise((resolve) => {
requestQueue.push((newToken) => {
config.headers.Authorization = `Bearer ${newToken}`;
resolve(axios(config)); // 换上新 token 后重新执行
});
});
}
}
return Promise.reject(error);
});