Nuxt.js 中间件实战:实现路由守卫与权限控制

作者:南栀

时间:2026-11-06

浏览:97

深入讲解 Nuxt.js 路由中间件的运作机制与权限控制方案,涵盖全局守卫、页面命名中间件、双端凭证同步、RBAC角色判定与常见陷阱排查。

文章目录

一、Nuxt 路由中间件的核心分类与执行时机

在传统的 Vue 单页应用(SPA)中,路由守卫通常在 vue-router 的 beforeEach 钩子中集中声明。而在 Nuxt.js 中,路由拦截由专门的“路由中间件(Route Middleware)”承载。它们既会在客户端单页路由切换时执行,也会在首屏服务端渲染(SSR)处理请求时直接在服务器端运行。

Nuxt 3 提供了三种类型的路由中间件,它们在工程规范和适用范围上有清晰的划分:

  • 全局路由中间件:存放在 middleware/ 目录下,并以 *.global.ts 结尾(例如 middleware/01.auth.global.ts)。应用中每一次路由变动都会无差别自动执行此类中间件,执行次序由文件名的字母和数字序决定。
  • 页面命名中间件:存放在 middleware/ 目录下,但不带 .global 后缀(例如 middleware/auth.ts)。它们不会全局触发,只有在目标页面组件中通过 definePageMeta({ middleware: 'auth' }) 显式挂载时才会生效。
  • 匿名/行内中间件:直接在页面的 definePageMeta 中以内联函数的形式声明,通常用于某些仅在单一页面使用的特例判断逻辑。
Nuxt.js 中间件实战:实现路由守卫与权限控制
图 1:Nuxt 3 路由中间件执行时机与生命周期管道架构图

中间件函数的签名非常统一:接收目标路由 to 与来源路由 from 两个参数。中间件通过其返回值来决定路由命运:

  • 返回 undefined 或不返回任何内容:代表校验通过,放行路由导航;
  • 返回 navigateTo(path, [options]):终止当前路由,重定向至新地址;
  • 返回 abortNavigation(error):中止本次导航,并可选择抛出指定 HTTP 状态码的错误。

二、权限守卫实战:从登录状态到角色鉴权(RBAC)

为了让权限逻辑高内聚、易维护,我们按照“状态组合式函数(Composable)→ 页面元数据(Page Meta)声明 → 守卫中间件(Middleware)校验”的标准流程进行工程化落地。

步骤 1:使用 Composable 与 useCookie 管理双端登录态

Nuxt 是同构环境,若使用浏览器专属的 localStorage 存放登录凭证,服务端在首次执行中间件时将无法读取,进而造成首屏与客户端渲染结果冲突。我们使用 Nuxt 内置的 useCookie 来维护用户凭证与角色信息。

在项目中创建用户状态管理文件 composables/useAuth.ts:

// composables/useAuth.ts
export interface UserInfo {
  id: string
  name: string
  roles: string[]
}

export const useAuth = () => {
  // useCookie 在 SSR 阶段自动读取 Cookie 头,在客户端自动同步 document.cookie
  const token = useCookie<string | null>('auth_token', {
    default: () => null,
    maxAge: 60 * 60 * 24 * 7, // 7 天有效期
    sameSite: 'lax',
    secure: process.env.NODE_ENV === 'production'
  })

  const userInfo = useState<UserInfo | null>('user_info', () => null)

  const isAuthenticated = computed(() => Boolean(token.value))

  const hasRole = (role: string | string[]) => {
    if (!userInfo.value || !userInfo.value.roles) return false
    const roleList = Array.isArray(role) ? role : [role]
    return roleList.some(r => userInfo.value!.roles.includes(r))
  }

  const logout = () => {
    token.value = null
    userInfo.value = null
    return navigateTo('/login')
  }

  return {
    token,
    userInfo,
    isAuthenticated,
    hasRole,
    logout
  }
}

步骤 2:编写权限守卫命名中间件

接着创建通用认证中间件 middleware/auth.ts。该中间件将检验目标路由是否需要认证、用户是否登录,并在未登录时把原始访问路径作为参数传递给登录页,以便登录成功后无缝跳回。

// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
  const { isAuthenticated } = useAuth()

  // 1. 如果用户未登录,重定向到登录页面并记录当前访问地址
  if (!isAuthenticated.value) {
    // 避免目标页面已是 /login 时陷入重复跳转死循环
    if (to.path === '/login') {
      return
    }
    return navigateTo({
      path: '/login',
      query: {
        redirect: to.fullPath
      }
    })
  }
})

步骤 3:编写基于角色的细粒度中间件(RBAC)

对于需要特定权限的后台页面(例如系统管理页),我们可以扩展专用中间件 middleware/admin.ts。这里演示通过读取路由配置中的 to.meta.roles 完成授权判定:

// middleware/admin.ts
export default defineNuxtRouteMiddleware((to) => {
  const { isAuthenticated, hasRole } = useAuth()

  // 依赖先置条件:必须已经登录
  if (!isAuthenticated.value) {
    return navigateTo({
      path: '/login',
      query: { redirect: to.fullPath }
    })
  }

  // 提取目标路由声明的所需角色列表
  const requiredRoles = (to.meta.roles as string[]) || ['admin']

  // 校验当前用户是否具有对应权限
  if (!hasRole(requiredRoles)) {
    // 如果没有权限,中止导航并直接抛出 403 权限拒绝异常
    return abortNavigation(
      createError({
        statusCode: 403,
        statusMessage: 'Forbidden: 您没有访问该受保护资源的权限'
      })
    )
  }
})
Nuxt.js 中间件实战:实现路由守卫与权限控制
图 2:Nuxt 路由权限守卫决策与流转操作步骤流程图

步骤 4:在页面组件中通过 definePageMeta 声明守卫

在具体的业务页面中,利用编译宏 definePageMeta 将中间件挂载至路由上。例如管理后台仪表盘页面 pages/admin/dashboard.vue:

<!-- pages/admin/dashboard.vue -->
<script setup lang="ts">
definePageMeta({
  // 按照顺序挂载中间件:先验证登录身份,再核实管理员角色
  middleware: ['auth', 'admin'],
  // 在 meta 中定义本页面要求的角色要求
  roles: ['superadmin', 'admin']
})

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

<template>
  <div class="dashboard-page">
    <h1>系统管理控制台</h1>
    <p>当前操作员:{{ userInfo?.name }} (角色: {{ userInfo?.roles.join(', ') }})</p>
    <button @click="logout">安全退出</button>
  </div>
</template>

三、容易混淆的认知辨析与关键陷阱

辨析 1:“有前端路由中间件,页面接口数据就安全了”

这种说法看似合理,因为在日常使用中,未登录的用户点击链接确实会被中间件重定向拦截,根本看不到后台页面的 DOM 结构。但这种认识存在根本局限。

成立的条件:路由中间件保障的是“UI 视图层级的访问流程与展示体验”,它阻止的是浏览器渲染受保护的 Vue 视图组件。

需要修正的地方:前端路由拦截完全运行在客户端或服务端渲染阶段的展示层。恶意调用者完全可以直接向 Nuxt 的服务端接口(例如 /api/admin/users)或外部后端直接发送 HTTP 请求,绕过前端中间件。

安全准则
路由中间件是用户界面的门禁,而 server/api/* 以及 server/middleware/* 才是真正的数据安全边界。所有涉及敏感数据的接口都必须在服务端独立完成 Token 解密和鉴权。

辨析 2:“Token 存 localStorage 只要只在客户端读取就行”

部分初学者习惯把 Token 存入 localStorage,并在中间件内包裹 if (process.client) 进行校验。这种做法看似跑通了流程,但在生产环境下会引发严重的水合闪烁问题。

Nuxt.js 中间件实战:实现路由守卫与权限控制
图 3:SSR 服务端与客户端不同存储机制的表现差异对比

当用户直接在浏览器地址栏输入受保护页面 URL 时,请求首先到达 Nuxt SSR 服务器。由于服务器环境没有 window.localStorage,此时服务端中间件无法拿到凭证,只能假设用户未登录并渲染重定向到登录页的 HTML;然而到达客户端后,水合阶段又发现 localStorage 存在 Token,试图拉回目标页面。这种不一致会导致屏幕剧烈跳闪,甚至引发 Vue 3 水合失败警告(Hydration Mismatch)。

解决依据:采用标准 Cookie 机制。由于 Cookie 会在每次 HTTP 请求的 Header 中自动送达服务端,Nuxt 的 useCookie 可以同时在服务端与客户端无缝提取相同的数据,实现真正的双端同构鉴权。

陷阱 3:重定向死循环(Infinite Redirect Loop)

如果使用了全局中间件(如 *.global.ts),但未对白名单页面进行排除,当未登录用户访问受保护路由被重定向到 /login 时,进入 /login 又会再次触发该全局中间件,从而无限循环重定向。

排查与修复方法:在中间件顶部显式判断目标路由路径或通过白名单放行:

// 检查当前访问目标是否为无需鉴权的公开白名单
const publicPages = ['/login', '/register', '/forgot-password']

if (!isAuthenticated.value && !publicPages.includes(to.path)) {
  return navigateTo({
    path: '/login',
    query: { redirect: to.fullPath }
  })
}

四、落地方案总结与工程建议

在实际项目中实现 Nuxt 路由守卫与权限控制,应当遵循明确的工程规范与分层原则。下表总结了各层级的核心关注点与适用场景:

维度 全局中间件 (*.global.ts) 页面命名中间件 (middleware/*.ts) 服务端中间件 (server/middleware/*)
触发范围 全站所有路由跳转均触发 仅在声明了 definePageMeta 的页面触发 所有向服务器发起的数据请求均触发
主要职责 全局统计埋点、基础环境初始化、公开白名单初筛 业务权限控制、用户角色校验(RBAC)、登录拦截 API 凭证鉴权、会话保护、跨域与请求头过滤
凭证依赖 useCookie useCookie / Pinia 状态 getCookie(event) / getHeader(event)
失败处理 return navigateTo('/login') return abortNavigation(error) throw createError({ statusCode: 401 })
Nuxt.js 中间件实战:实现路由守卫与权限控制
图 4:Nuxt 权限控制体系与防御纵深总结架构图

通过将页面级权限判定(基于 useCookie 与 defineNuxtRouteMiddleware)与服务端接口防护(基于 Nitro server/middleware)有机结合,既能为合法用户提供流畅、无水合跳闪的单页交互体验,又能杜绝任何绕过前端界面窃取接口数据的安全风险。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章