SPA 安全登录:从“玩具”到“产品”的关键一步

有朋友问:“SPA 怎么做安全登录?”你可以微微一笑,说:“先设 HttpOnly Cookie,再配个探测接口,前端用 credentials: 'include',路由守卫一包,401 拦截器一兜——齐活。”

为什么传统 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'

在使用 fetchaxios 时,必须显式开启凭据携带:

// 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 安全登录的五个关键实践:

  1. HttpOnly Cookie —— 安全存储凭证;
  2. 探测接口 /api/me —— 统一身份验证入口;
  3. credentials: 'include' —— 确保凭据随请求发送;
  4. 路由守卫 —— 前端访问控制;
  5. 401 拦截器 —— 异常兜底处理。

这套方案不仅适用于纯客户端 SPA,更能无缝支持 Nuxt、Next.js 等 SSR 框架,真正实现“安全”与“体验”的双赢。

本站简介

聚焦于全栈技术和量化技术的技术博客,分享软件架构、前后端技术、量化技术、人工智能、大模型等相关文章总结。