为什么传统 localStorage + JWT 不够用了?
很多初学者习惯在 SPA 中使用 localStorage 存储 JWT Token,每次请求手动带上 Authorization: Bearer <token>。这种方式看似简单,实则隐患重重:
- XSS 风险高:一旦页面被注入恶意脚本,
localStorage中的 Token 极易被盗取。 - SSR 不友好:在 Nuxt、Next.js 等支持服务端渲染(SSR)的框架中,服务端无法读取
localStorage,导致首屏渲染时“误判”用户未登录,可能错误跳转或显示空白内容。
因此,对于现代同构应用(Universal App),我们需要更稳健的方案。
用 HttpOnly Cookie 替代 localStorage
核心思想是:将身份凭证(如 Session ID 或加密 Token)写入 HttpOnly Cookie。
HttpOnly属性禁止 JavaScript 读取 Cookie,从根本上防御 XSS 攻击。- 浏览器会自动在每次请求中携带该 Cookie(包括 SSR 首屏请求),服务端无感获取用户身份。
例如,在 Nuxt 的 Nitro 后端中,登录成功后可这样设置:
setCookie(event, "auth_token", JSON.stringify(userSession), {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
path: "/",
});
这样,无论是客户端 AJAX 请求,还是服务端 SSR 渲染 /dashboard 页面,都能通过 Cookie 自动传递身份信息。
提供 /api/me 探测接口
前端需要知道“当前用户是否已登录”。为此,后端应提供一个轻量级的探测接口,如 /api/me:
- 若 Cookie 有效,返回用户基本信息(如
{ id, username }); - 若无效或过期,返回
401 Unauthorized。
这个接口是前端判断登录状态、渲染用户头像或跳转登录页的依据。
前端请求配置 credentials: 'include'
在使用 fetch 或 axios 时,必须显式开启凭据携带:
// fetch
fetch("/api/me", { credentials: "include" });
// axios
axios.defaults.withCredentials = true;
否则,浏览器默认不会发送 Cookie(出于安全策略),导致即使后端设置了 Cookie,前端也无法“证明”自己已登录。
路由守卫拦截未授权访问
在 Vue Router、Angular Router 或 Nuxt 的 middleware 中,加入路由守卫:
- 访问受保护页面(如
/profile)前,先调用/api/me; - 若返回 401,则重定向到
/login。
例如,在 Nuxt 中可编写全局中间件:
export default defineNuxtRouteMiddleware(async (to) => {
if (to.meta.requiresAuth) {
try {
await $fetch("/api/me", { credentials: "include" });
} catch (error) {
return navigateTo("/login");
}
}
});
这确保了无论用户直接输入 URL 还是点击链接,未登录者都无法进入敏感页面。
401 拦截器兜底处理 Token 失效
即使有路由守卫,某些 API 请求仍可能因 Token 过期而返回 401。此时需全局响应拦截器:
- 捕获 401 响应;
- 清除本地缓存(如用户信息);
- 跳转至登录页。
在 Axios 中实现如下:
axios.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem("userInfo");
window.location.href = "/login";
}
return Promise.reject(error);
},
);
这形成了最后一道防线,避免用户停留在“半登录”状态。
总结:安全不是功能,而是架构
回到开头那句“黑话”,其实它浓缩了现代 SPA 安全登录的五个关键实践:
HttpOnly Cookie—— 安全存储凭证;- 探测接口
/api/me—— 统一身份验证入口; credentials: 'include'—— 确保凭据随请求发送;- 路由守卫 —— 前端访问控制;
- 401 拦截器 —— 异常兜底处理。
这套方案不仅适用于纯客户端 SPA,更能无缝支持 Nuxt、Next.js 等 SSR 框架,真正实现“安全”与“体验”的双赢。