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>的前提。虽然fetchAPI 原生可用,但它缺乏拦截器机制,无法实现“自动添加 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_tokenCookie 无法被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_tokenCookie,而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_tokenCookie 查询用户并返回基本信息。这样,服务端就能在渲染前就知道用户是谁,从而决定是否显示“欢迎,张三”。 -
logout不尝试删除HttpOnlyCookie 。这是正确的做法。前端 JavaScript 无法删除HttpOnlyCookie,强行调用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双向绑定 。简洁地将表单数据与formref 关联,无需手动@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
|
| **登录后跳转到 |

9668

被折叠的 条评论
为什么被折叠?



