Nuxt 3 认证实现:JWT + HttpOnly Cookie + Axios 拦截器实战

1. 项目概述:为什么在 Nuxt.js 里做认证不是“加个登录页”那么简单

Nuxt.js 是一个基于 Vue 的服务端渲染(SSR)框架,它天然具备前后端同构能力——页面既能在服务端预渲染,也能在客户端动态交互。但正是这个优势,让认证(Authentication)成了它最棘手的“甜蜜负担”。很多人第一次尝试时会发现:登录后刷新页面,用户状态突然消失了;API 请求返回 401,但控制台没报错;用 Axios 发送带 token 的请求,后端却收不到 Authorization 头;甚至在 SSR 模式下, $auth 对象在 asyncData fetch 中是 undefined ……这些都不是代码写错了,而是对 Nuxt 认证生命周期、上下文隔离、token 传递链路的理解出现了断层。

我从 2019 年开始用 Nuxt 2 搭建 SaaS 后台,到 2022 年全面迁移到 Nuxt 3,踩过所有主流认证路径的坑:自己手写 $auth 插件、用 @nuxtjs/auth-next (已归档)、试过 @sidebase/nuxt-auth (基于 NextAuth.js),也深度定制过基于 JWT 的轻量方案。最终沉淀出一套不依赖第三方模块、逻辑透明、可调试、可审计的认证实现方式——它不追求“一行代码搞定”,而是把每个环节的职责、边界和数据流向都摊开来讲清楚。这篇文章就是围绕标题“How To Implement Authentication in a Nuxt.js App”展开的完整实操复盘:不讲抽象概念,只讲你在 pages/login.vue 里敲下的每一行代码背后发生了什么;不堆砌配置项,只解释为什么 axios.defaults.headers.common['Authorization'] 在 SSR 下必须延迟设置;不回避 JWT 的缺陷,而是告诉你如何用 HttpOnly Cookie + short-lived JWT 组合规避 XSS 和 token 泄露风险。适合正在用 Nuxt 开发中后台、SaaS 应用或需要用户体系的独立开发者,尤其适合那些已经能写组件、调 API,但一碰登录态就卡壳的中级前端。

2. 整体设计思路:拒绝黑盒模块,构建可追溯的认证流

2.1 为什么放弃 @nuxtjs/auth-next ?三个真实痛点

@nuxtjs/auth-next 曾是 Nuxt 2 时代的事实标准,但它在 Nuxt 3 生态中已被官方明确标记为“archived”,不再维护。更重要的是,它在实际项目中暴露出三个无法绕过的硬伤:

第一, 上下文污染严重 。该模块会全局注入 $auth 实例,并在 nuxtServerInit 中自动读取 Cookie 初始化状态。问题在于,它把“身份校验”、“token 刷新”、“路由守卫”全部耦合在一个对象里。当你需要在某个页面单独验证一个临时 token(比如邮箱确认链接里的签名 token),或者想对不同 API 分组使用不同认证策略(如管理后台用 JWT,文件上传用短期预签名 URL),就会发现 $auth 的 API 设计根本不支持这种细粒度控制。我曾在一个客户项目中被迫 fork 仓库,重写其 refreshTokens 方法,只为支持自定义刷新接口的响应结构——这显然违背了“开箱即用”的初衷。

第二, SSR 渲染与客户端 hydration 不一致 。这是最隐蔽也最致命的问题。 @nuxtjs/auth-next 在服务端通过 cookie 读取 token 并调用 /api/auth/user 获取用户信息,但它的 user 数据默认不序列化到 window.__NUXT__ 中。结果就是:服务端渲染的页面显示了用户头像和昵称,但客户端 JS 加载完成后, $auth.user 突然变成 null ,触发一次不必要的重定向或 UI 闪烁。我们花了整整两天排查,最终发现是模块内部 store 的 SSR 同步逻辑存在 race condition——当 fetch 请求比 nuxtServerInit 完成得慢时,客户端 store 就拿不到初始值。

第三, 调试成本极高 。它的错误提示极其笼统,比如 auth: error on login ,你根本不知道是 Axios 请求失败、token 解析异常,还是后端返回了非标准格式的错误体。在生产环境遇到 JWT is not well formed, there are no dots 这类错误时,你甚至无法定位是哪个环节(前端拼接?后端签发?中间件拦截?)出了问题。而真实项目中,认证错误日志恰恰是最关键的可观测性入口。

提示:如果你的项目仍处于 Nuxt 2 阶段且无迁移计划, @nuxtjs/auth-next 仍是可用选项,但务必重写其 plugins/auth.js ,将 user 数据显式 commit 到 Vuex store 并确保 window.__NUXT__ 包含完整初始状态。不过,本文聚焦 Nuxt 3,我们将完全绕过这类黑盒模块,从零构建。

2.2 我们的设计原则:分层解耦,各司其职

基于上述教训,我为当前项目确立了四条铁律:

第一,认证逻辑与业务逻辑物理隔离 。绝不把 login() 方法直接写在 login.vue methods 里。而是创建 composables/useAuth.ts ,封装所有与 token、用户、会话相关的操作。这样,同一个登录逻辑可以被 login.vue register.vue 、甚至 server/api/auth/login.post.ts (用于服务端直连认证)复用,避免重复造轮子。

第二,Token 管理权交给浏览器原生机制 。不把 JWT 存在 localStorage sessionStorage ——这是 pop3 server allows plain text authentication vulnerability 类问题的温床(前端存储的 token 可被 XSS 脚本轻易窃取)。我们采用 HttpOnly + Secure + SameSite=Lax 的 Cookie 存储 refresh token,而 access token 仅存在于内存( ref<string> )中,每次请求前由 Axios 拦截器动态注入。这样,即使页面被 XSS 攻击,攻击者也无法读取 HttpOnly Cookie,而内存中的 access token 在页面关闭后自动销毁。

第三,SSR 与 CSR 的认证状态必须可预测 。服务端渲染时,我们通过 useRequestHeaders 读取 cookie 头,调用后端 /api/auth/me 接口获取当前用户,并将结果作为 useState 的初始值传入。客户端 hydration 时, useState 会自动接管这个状态,保证首屏渲染和后续交互的用户数据完全一致。整个过程不依赖任何魔法变量,所有数据流清晰可见。

第四,错误处理必须精确到具体环节 。登录失败时,我们区分三种错误类型: NETWORK_ERROR (网络不通)、 AUTH_ERROR (账号密码错误)、 VALIDATION_ERROR (表单校验失败)。每种错误对应不同的 UI 反馈和埋点事件,而不是笼统地弹一个“登录失败,请重试”。

这套设计不追求“最简”,而是追求“最稳”——在复杂业务迭代中,稳定性和可维护性永远比开发速度重要。

2.3 技术栈选型依据:为什么是 JWT + Axios + Nuxt 3 Composition API

  • JWT(JSON Web Token) :它是目前最成熟的无状态认证方案。后端签发一个包含 sub (用户ID)、 exp (过期时间)、 iat (签发时间)等标准字段的 token,前端无需维护 session 表,所有校验逻辑可由后端统一完成。虽然存在 jwt伪造 风险,但只要后端使用强密钥(如 256 位 HMAC-SHA256)并严格校验 iss (签发者)、 aud (受众)字段,风险可控。相比 caching_sha2_password 这类数据库认证插件,JWT 更适合分布式微服务架构。

  • Axios :它是 Nuxt 生态中最成熟、文档最全的 HTTP 客户端。 axios官网 明确支持请求/响应拦截器、取消令牌、自动 JSON 序列化等特性。最关键的是,它允许我们精细控制每个请求的 headers——这正是注入 Authorization: Bearer <token> 的前提。虽然 fetch API 原生可用,但它缺乏拦截器机制,无法实现“自动添加 token”这一核心需求。

  • Nuxt 3 Composition API useAuth useState useCookie 等组合式 API 天然支持响应式状态管理。 useState 的 SSR 支持让我们能安全地在服务端获取用户数据并同步到客户端,彻底解决 hydration 不一致问题。而 defineNuxtPlugin 提供的插件机制,让我们能把 Axios 实例和认证逻辑优雅地注入到整个应用上下文,无需在每个页面手动 import。

这三者的组合,不是为了炫技,而是为了解决一个具体问题:在 Nuxt 的 SSR/CSR 混合渲染模型下,如何让认证状态既安全又可靠地贯穿整个请求生命周期。

3. 核心细节解析:从 Cookie 设置到 Token 注入的完整链路

3.1 后端配合:JWT 签发与 Cookie 设置规范

前端认证的成败,一半取决于后端是否按规范输出。我们以 Node.js + Express 为例,说明关键接口的设计要点:

// server/api/auth/login.post.ts (Nuxt 3 Server API)
export default defineEventHandler(async (event) => {
  const body = await readBody(event)
  const { email, password } = body

  // 1. 校验账号密码(此处省略 DB 查询)
  const user = await validateUser(email, password)
  if (!user) {
    throw createError({
      statusCode: 401,
      statusMessage: 'Invalid credentials'
    })
  }

  // 2. 生成 Access Token(短时效,15分钟)
  const accessToken = jwt.sign(
    { sub: user.id, email: user.email },
    useRuntimeConfig().jwtSecret,
    { expiresIn: '15m', issuer: 'my-app', audience: 'frontend' }
  )

  // 3. 生成 Refresh Token(长时效,7天),存入 Redis 或 DB
  const refreshToken = crypto.randomUUID()
  await setRefreshToken(user.id, refreshToken, 7 * 24 * 60 * 60) // 7 days in seconds

  // 4. 设置 HttpOnly Cookie(关键!)
  setCookie(event, 'refresh_token', refreshToken, {
    httpOnly: true,     // 前端 JS 无法读取
    secure: true,       // 仅 HTTPS 传输
    sameSite: 'lax',    // 防止 CSRF
    maxAge: 7 * 24 * 60 * 60, // 7 days
    path: '/api/'       // 限定作用域,避免泄露给前端静态资源
  })

  // 5. 返回 Access Token(仅此一项,不返回 refresh token!)
  return { accessToken, user: { id: user.id, email: user.email, name: user.name } }
})

这里有几个必须注意的细节:

  • httpOnly: true 是安全底线 。它确保 refresh_token Cookie 无法被 document.cookie 读取,从根本上杜绝了 XSS 窃取。这也是为什么我们不把 refresh token 存在 localStorage——那等于把钥匙挂在门上。

  • sameSite: 'lax' 是 CSRF 防护的关键 。它允许用户从其他网站点击链接访问你的首页(GET 请求携带 Cookie),但会阻止跨站 POST、PUT 等危险请求携带 Cookie。对于登录、登出这类敏感操作,后端还需额外校验 Origin 头。

  • path: '/api/' 限定了 Cookie 的作用域 。这意味着只有 /api/** 路径下的请求才会自动携带该 Cookie,而 / (首页)、 /login 等前端页面不会收到它,进一步缩小了攻击面。

  • 绝不返回 refresh_token 到前端 body 。很多教程犯这个错误,把 refresh token 也塞进 JSON 响应里。这是严重的设计失误——一旦响应被 XSS 脚本截获, refresh_token 就彻底暴露。正确的做法是只通过 Set-Cookie 头下发,前端完全感知不到它的存在。

  • Access Token 必须包含 issuer audience 字段 。这能防止 jwt 认证除了bearer 头 还可以用哪些 场景下的 token 误用。比如,一个为 mobile-app 签发的 token,不应该被 frontend 接受。

3.2 前端状态管理: useState 如何安全承载用户数据

在 Nuxt 3 中, useState 是管理跨组件、跨 SSR 的响应式状态的首选。我们创建 composables/useAuthState.ts

// composables/useAuthState.ts
import { useState, useCookie } from '#app'

// 创建一个全局可访问的用户状态
export const useAuthUser = () => {
  // 注意:name 必须是字符串字面量,不能是变量,否则 SSR 会失效
  const user = useState<any>('auth-user', () => null)
  
  // 从 Cookie 读取 refresh token(仅用于判断是否已登录,不用于发送请求)
  const refreshToken = useCookie<string | undefined>('refresh_token')

  // 计算属性:用户是否已登录
  const isLoggedIn = computed(() => {
    // 服务端:检查 Cookie 是否存在且非空
    // 客户端:检查 user 是否有值(SSR 后 hydration 会填充)
    return !!refreshToken.value || !!user.value?.id
  })

  return {
    user,
    isLoggedIn,
    refreshToken
  }
}

这个 useAuthUser 的精妙之处在于:

  • useState 的 name 参数必须是字符串字面量 。如果你写成 useState('auth-' + Math.random()) ,SSR 时服务端生成的 window.__NUXT__ 里会存一个随机 key,而客户端 hydration 时找不到对应 key,导致状态丢失。这是新手最容易踩的坑。

  • isLoggedIn 是一个计算属性,而非普通 ref 。它同时检查 refreshToken (服务端凭证)和 user (客户端数据),确保在任何渲染阶段都能给出准确的登录态。比如,在用户刚打开首页时, refreshToken 存在但 user 还未加载, isLoggedIn 仍为 true ,我们可以显示“加载中...”,而不是闪一下“未登录”再变“已登录”。

  • refreshToken useCookie 而非 useState 。因为 refreshToken 本身不应被修改(前端无权修改 HttpOnly Cookie),我们只需要读取它来判断登录态。 useCookie 会自动监听 Cookie 变化,比手动 document.cookie 解析更可靠。

3.3 Axios 拦截器:如何在正确时机注入 Authorization 头

这是整个认证流程中最容易出错的一环。很多开发者直接在 plugins/axios.ts 里写:

// ❌ 错误示范:在插件初始化时就设置默认 header
export default defineNuxtPlugin((nuxtApp) => {
  const axios = createAxios()
  const { user } = useAuthUser() // ❌ 此时 user 还未初始化!
  axios.defaults.headers.common['Authorization'] = `Bearer ${user.value?.token}`
  return { provide: { axios } }
})

问题在于, useAuthUser() 在插件执行时( defineNuxtPlugin 回调内)无法获取到 SSR 传递的初始 user 状态, user.value null ,导致所有请求都带上 Bearer null ,后端直接 401。

正确做法是: 利用 Axios 的请求拦截器,在每次请求发出前动态读取当前 token 。我们创建 plugins/axios-auth.ts

// plugins/axios-auth.ts
export default defineNuxtPlugin((nuxtApp) => {
  const axios = nuxtApp.$axios

  // 请求拦截器:在发送前注入 Authorization 头
  axios.interceptors.request.use(
    async (config) => {
      // 1. 跳过不需要认证的请求(如登录、注册、公开资源)
      const publicPaths = ['/api/auth/login', '/api/auth/register', '/api/public/']
      if (publicPaths.some(path => config.url?.includes(path))) {
        return config
      }

      // 2. 从 useState 获取当前用户,读取其 access token
      const { user } = useAuthUser()
      const token = user.value?.accessToken

      // 3. 如果有 token,注入到 Authorization 头
      if (token) {
        config.headers.Authorization = `Bearer ${token}`
      }

      return config
    },
    (error) => {
      return Promise.reject(error)
    }
  )

  // 响应拦截器:处理 401 错误,触发 token 刷新
  axios.interceptors.response.use(
    (response) => response,
    async (error) => {
      const { user, refreshToken } = useAuthUser()
      const originalRequest = error.config

      // 仅对 401 且未重试过的请求进行刷新
      if (error.response?.status === 401 && !originalRequest._retry) {
        originalRequest._retry = true

        try {
          // 调用刷新接口,传入当前 refresh token
          const refreshRes = await $fetch('/api/auth/refresh', {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json'
            },
            body: { refreshToken: refreshToken.value }
          })

          // 更新用户状态中的 access token
          if (refreshRes.accessToken && user.value) {
            user.value.accessToken = refreshRes.accessToken
          }

          // 重发原始请求
          return axios(originalRequest)
        } catch (refreshError) {
          // 刷新失败,清空状态,跳转登录页
          user.value = null
          navigateTo('/login')
          return Promise.reject(refreshError)
        }
      }

      return Promise.reject(error)
    }
  )
})

这个拦截器的关键点:

  • originalRequest._retry 是防重入标志 。没有它,当刷新 token 失败时, navigateTo('/login') 会触发新请求,新请求又 401,再次进入拦截器,形成无限循环。 _retry 字段是 Axios 允许我们在 config 上挂载任意属性的特性,用来标记该请求是否已被重试过。

  • $fetch 用于刷新请求,而非 axios 。因为刷新接口 /api/auth/refresh 需要携带 refresh_token Cookie,而 axios 实例在拦截器里调用时,其 withCredentials 选项可能未正确设置。 $fetch 是 Nuxt 内置的、对 Cookie 支持更友好的客户端,它会自动携带当前域名下的所有 Cookie(包括 HttpOnly 的),确保 refresh token 能被后端正确读取。

  • navigateTo('/login') 必须在 catch 块中 。这是用户主动退出或 token 彻底失效时的兜底方案。注意, navigateTo 在服务端是无效的,所以这个逻辑只在客户端执行,符合预期。

4. 实操过程详解:从零搭建可运行的认证系统

4.1 初始化项目与基础配置

首先,确保你使用的是 Nuxt 3.10+(推荐最新稳定版)。创建项目:

npx nuxi@latest init my-auth-app
cd my-auth-app
npm install

安装 Axios(Nuxt 3 已内置 $fetch ,但我们需要 Axios 的拦截器能力):

npm install axios

nuxt.config.ts 中配置 Axios:

// nuxt.config.ts
export default defineNuxtConfig({
  modules: [
    '@nuxtjs/axios' // 注意:这是 Nuxt 3 的 @nuxtjs/axios 模块,非 Nuxt 2 版本
  ],
  axios: {
    // 配置基础 URL,避免在每个请求里写全路径
    baseURL: '/api'
  }
})

注意:Nuxt 3 的 @nuxtjs/axios 模块与 Nuxt 2 的完全不同,它只是对 axios 库的轻量封装,不包含任何认证逻辑,完全符合我们“去黑盒化”的原则。

4.2 创建认证组合式函数: useAuth.ts

这是整个认证系统的核心。创建 composables/useAuth.ts

// composables/useAuth.ts
import { ref, computed, onMounted } from 'vue'
import { useAuthUser } from './useAuthState'
import { $fetch } from 'nitropack'

// 定义登录参数类型
interface LoginParams {
  email: string
  password: string
}

// 定义用户类型
interface User {
  id: string
  email: string
  name: string
  accessToken: string
}

// 主认证 Hook
export const useAuth = () => {
  const { user, isLoggedIn, refreshToken } = useAuthUser()
  const loading = ref(false)
  const error = ref<string | null>(null)

  // 登录方法
  const login = async (params: LoginParams) => {
    loading.value = true
    error.value = null

    try {
      // 调用后端登录接口
      const res = await $fetch<{ accessToken: string; user: User }>('/auth/login', {
        method: 'POST',
        body: params
      })

      // 成功后,更新用户状态
      user.value = {
        ...res.user,
        accessToken: res.accessToken
      }

      // 登录成功,跳转首页
      navigateTo('/')
    } catch (err: any) {
      // 统一错误处理
      if (err.status === 401) {
        error.value = '邮箱或密码错误'
      } else if (err.status === 422) {
        error.value = '请检查输入格式'
      } else {
        error.value = '网络错误,请稍后重试'
      }
    } finally {
      loading.value = false
    }
  }

  // 登出方法
  const logout = async () => {
    loading.value = true
    try {
      // 调用后端登出接口(可选,用于服务端清除 refresh token)
      await $fetch('/auth/logout', { method: 'POST' })
    } catch (e) {
      // 登出失败不影响前端状态清理
      console.warn('Logout failed:', e)
    } finally {
      // 清空前端状态
      user.value = null
      // 注意:HttpOnly Cookie 无法通过 JS 删除,只能等待过期或后端 Set-Cookie 覆盖
      navigateTo('/login')
      loading.value = false
    }
  }

  // 检查登录态(用于服务端预取)
  const checkAuth = async () => {
    if (import.meta.server) {
      // 服务端:通过 cookie 读取并调用 /auth/me
      const headers = useRequestHeaders(['cookie'])
      try {
        const me = await $fetch<User>('/auth/me', {
          headers: { cookie: headers.cookie || '' }
        })
        user.value = me
      } catch (e) {
        user.value = null
      }
    } else {
      // 客户端:如果 user 已存在,无需操作;否则,可发起 /auth/me 请求
      if (!user.value && refreshToken.value) {
        try {
          const me = await $fetch<User>('/auth/me')
          user.value = me
        } catch (e) {
          user.value = null
        }
      }
    }
  }

  // 暴露给模板使用的属性和方法
  return {
    user,
    isLoggedIn,
    loading,
    error,
    login,
    logout,
    checkAuth
  }
}

这个 useAuth 的设计亮点:

  • checkAuth 方法专为 SSR 优化 。在服务端,它通过 useRequestHeaders(['cookie']) 安全地读取请求头中的 cookie ,然后调用 /auth/me 接口。这个接口应该由后端实现,根据 refresh_token Cookie 查询用户并返回基本信息。这样,服务端就能在渲染前就知道用户是谁,从而决定是否显示“欢迎,张三”。

  • logout 不尝试删除 HttpOnly Cookie 。这是正确的做法。前端 JavaScript 无法删除 HttpOnly Cookie,强行调用 document.cookie = 'refresh_token=; expires=Thu, 01 Jan 1970 00:00:00 GMT' 是无效的。正确的做法是:后端 /auth/logout 接口返回一个 Set-Cookie: refresh_token=; Max-Age=0 的响应头,强制浏览器立即过期该 Cookie。

  • 错误分类明确 401 对应凭证错误, 422 对应表单校验失败(如邮箱格式不对),其他情况归为网络错误。这让你可以在 UI 上给出精准反馈,而不是一个模糊的“登录失败”。

4.3 构建登录页面: pages/login.vue

现在,我们把 useAuth 应用到实际页面中:

<!-- pages/login.vue -->
<template>
  <div class="login-container">
    <h1>用户登录</h1>
    <form @submit.prevent="handleSubmit" class="login-form">
      <div class="form-group">
        <label for="email">邮箱</label>
        <input
          id="email"
          v-model="form.email"
          type="email"
          required
          placeholder="your@email.com"
        />
      </div>

      <div class="form-group">
        <label for="password">密码</label>
        <input
          id="password"
          v-model="form.password"
          type="password"
          required
          placeholder="••••••••"
        />
      </div>

      <div v-if="error" class="error-message">
        {{ error }}
      </div>

      <button type="submit" :disabled="loading" class="login-btn">
        {{ loading ? '登录中...' : '登录' }}
      </button>
    </form>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useAuth } from '@/composables/useAuth'

const form = ref({
  email: '',
  password: ''
})

const { login, loading, error } = useAuth()

const handleSubmit = () => {
  login(form.value)
}

// 页面加载时,检查当前登录态,已登录则跳转
onMounted(() => {
  const { isLoggedIn } = useAuth()
  if (isLoggedIn.value) {
    navigateTo('/')
  }
})
</script>

<style scoped>
.login-container {
  max-width: 400px;
  margin: 2rem auto;
  padding: 2rem;
  border: 1px solid #eee;
  border-radius: 8px;
}
.form-group {
  margin-bottom: 1rem;
}
.form-group label {
  display: block;
  margin-bottom: 0.5rem;
  font-weight: bold;
}
.form-group input {
  width: 100%;
  padding: 0.5rem;
  border: 1px solid #ccc;
  border-radius: 4px;
}
.error-message {
  color: #d32f2f;
  background-color: #ffebee;
  padding: 0.5rem;
  border-radius: 4px;
  margin-bottom: 1rem;
}
.login-btn {
  width: 100%;
  padding: 0.75rem;
  background-color: #1976d2;
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 1rem;
  cursor: pointer;
}
.login-btn:disabled {
  background-color: #90a4ae;
  cursor: not-allowed;
}
</style>

这个页面的关键细节:

  • onMounted 中检查登录态 。这是用户体验的细节。用户如果已经登录,直接访问 /login 页面,应该自动跳转到首页,而不是让他再输一遍密码。 isLoggedIn.value 在服务端已根据 Cookie 计算好,客户端 hydration 后立即可用,所以跳转是瞬时的,没有闪烁。

  • @submit.prevent 阻止表单默认提交 。这是 Vue 的标准做法,确保我们完全控制提交逻辑。

  • v-model 双向绑定 。简洁地将表单数据与 form ref 关联,无需手动 @input 监听。

4.4 实现路由守卫:保护需要登录的页面

Nuxt 3 提供了 middleware 机制来实现路由守卫。创建 middleware/auth.global.ts (global 表示全局应用):

// middleware/auth.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
  // 获取登录态
  const { isLoggedIn } = useAuthUser()

  // 定义需要保护的路径(正则匹配)
  const protectedPaths = /^\/(dashboard|profile|settings)/

  // 如果目标路径需要保护,且用户未登录,则跳转到登录页
  if (protectedPaths.test(to.path) && !isLoggedIn.value) {
    return navigateTo('/login?redirect=' + encodeURIComponent(to.path))
  }
})

这个守卫的巧妙之处:

  • protectedPaths 使用正则表达式 。比写死一串 ['/dashboard', '/profile'] 更灵活,易于扩展。比如新增 /admin 路径,只需改正则,无需修改数组。

  • redirect 参数保留原始路径 。用户登录成功后,我们可以读取这个参数,自动跳转回他最初想去的页面,而不是固定跳转到首页。在 pages/login.vue login 成功回调中,可以这样处理:

// 在 useAuth.ts 的 login 方法中,登录成功后
const redirect = useRoute().query.redirect as string
navigateTo(redirect || '/')
  • defineNuxtRouteMiddleware 是 Nuxt 3 的标准 API 。它会在路由解析的早期阶段执行,比 beforeEach 更底层,性能更好,且天然支持 SSR。

4.5 服务端预取用户数据: server/middleware/auth.ts

为了让服务端渲染的页面能显示个性化内容(如顶部导航栏的用户名),我们需要在服务端就获取用户数据。创建 server/middleware/auth.ts

// server/middleware/auth.ts
export default defineEventHandler(async (event) => {
  // 读取请求头中的 cookie
  const headers = getHeaders(event)
  const cookie = headers.cookie || ''

  // 尝试调用 /api/auth/me 接口获取用户
  try {
    const user = await $fetch('/auth/me', {
      headers: { cookie }
    })
    
    // 将用户数据注入 event.context,供页面使用
    event.context.authUser = user
  } catch (e) {
    // 获取失败,设为 null
    event.context.authUser = null
  }
})

然后在需要显示用户信息的页面(如 app.vue )中,通过 useRequestEvent 读取:

<!-- app.vue -->
<template>
  <div>
    <nav>
      <NuxtLink to="/">首页</NuxtLink>
      <template v-if="authUser">
        <span>欢迎,{{ authUser.name }}!</span>
        <button @click="logout">登出</button>
      </template>
      <template v-else>
        <NuxtLink to="/login">登录</NuxtLink>
      </template>
    </nav>
    <NuxtPage />
  </div>
</template>

<script setup lang="ts">
import { useRequestEvent } from '#app'
import { useAuth } from '@/composables/useAuth'

const event = useRequestEvent()
const authUser = event?.context?.authUser || null

const { logout } = useAuth()
</script>

这样,服务端渲染的 HTML 中就已经包含了 <span>欢迎,张三!</span> ,用户无需等待 JS 加载就能看到个性化内容,极大提升了首屏体验。

5. 常见问题与排查技巧实录:那些年我们踩过的坑

5.1 问题速查表:高频故障现象与根因分析

现象 可能根因 排查步骤 解决方案
刷新页面后用户状态丢失 useState 的 name 不是字符串字面量,或 useAuthUser 在服务端未正确初始化 1. 检查 useState('auth-user') 的 name 是否为字面量
2. 查看 window.__NUXT__ 中是否有 auth-user 字段
3. 在 server/middleware/auth.ts 中打印 event.context.authUser
确保 useState name 是字面量;在服务端 middleware 中显式设置 event.context.authUser 并确保其值被正确序列化
Axios 请求始终不带 Authorization 请求拦截器未正确注册,或拦截的 URL 被 publicPaths 误判为公共路径 1. 在拦截器开头 console.log(config.url)
2. 检查 config.url 是否匹配 publicPaths 数组
3. 确认 plugins/axios-auth.ts 是否在 nuxt.config.ts plugins 数组中正确声明
console.log 加入拦截器调试;调整 publicPaths 正则或数组;检查插件注册顺序
refresh_token Cookie 未被后端读取 前端请求未设置 credentials: 'include' ,或后端 SameSite 设置不当 1. 浏览器 DevTools > Application > Cookies,确认 Cookie 存在且 HttpOnly 属性为 true
2. 检查请求的 Origin Referer
3. 后端 setCookie sameSite 是否为 'lax' 'strict'
前端 $fetch 默认携带 Cookie;后端 sameSite: 'lax' 兼容性最好;确保前后端域名一致(开发时用 localhost ,不要混用 127.0.0.1
jwt is not well formed, there are no dots 前端拼接的 token 字符串为空或格式错误,如 Bearer 后面没有内容 1. 在请求拦截器中 console.log('Token:', token)
2. 检查 user.value?.accessToken 是否为 undefined
3. 确认后端 /auth/login 返回的 accessToken 字段名是否一致
在拦截器中增加空值检查: if (token && token.trim()) { config.headers.Authorization = ... } ;统一后端返回字段名为 accessToken
**登录后跳转到
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值