Skip to main content

本地存储与鉴权

面试一句话总结:前端本地存储主要包括 Cookie(用于状态维持,4KB)、Web StoragelocalStorage/sessionStorage,5MB,纯前端使用)和 IndexedDB(本地数据库,存大量结构化数据)。在鉴权场景中,现代应用多使用 JWT,围绕 Token 的存储位置(防 XSS/CSRF)和无感刷新是高频考点。

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 窗口

    面试高频陷阱:

    1. 刷新页面会清空 sessionStorage 吗? 不会,恢复或强制刷新都会保留。
    2. 同源的两个 Tab 页共享 sessionStorage 吗? 绝对不共享!
    3. 从 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);
});