5.2 路由中间件与守卫


5.2 路由中间件与守卫

路修好了,卡口设在导航发生前。本节讲路由中间件三级体系(全局、命名、内联)与登录守卫的完整实现,包括服务端首航与客户端换页两段各自怎么跑——这是"交接班"视角下少数在两端都执行的逻辑之一。

中间件的三级体系

路由中间件(middleware 目录)在导航确认前执行,能读路由信息、改跳转目标、直接叫停。三个级别:

// middleware/auth.global.ts // 级别一:全局中间件——文件名带 .global 后缀,每次导航都执行 export default defineRouteMiddleware((to, from) => { const token = useCookie('session-token') // 未登录访问受保护区域:改道登录页,带上回跳地址 if (!token.value && to.path.startsWith('/account')) { return navigateTo(`/login?redirect=${to.path}`) } })
// middleware/admin-only.ts // 级别二:命名中间件——不带后缀,页面声明后才执行 export default defineRouteMiddleware((to) => { const user = useState('user', () => ({ role: 'member' })) if (user.value.role !== 'admin' && to.path.startsWith('/admin')) { // 无权限:不打扰页面,送去成员首页并留个提示 return navigateTo('/account', { redirectCode: 302 }) } })
<!-- pages/admin/settings.vue:声明使用命名中间件 --> <script setup> definePageMeta({ middleware: ['admin-only'], // 数组可挂多个,按序执行 }) </script> <template> <div>管理设置:只有管理员能看到这里</div> </template>
<!-- pages/special.vue --> <script setup> // 级别三:内联中间件——写在页面里,只管本页,适合一次性逻辑 definePageMeta({ middleware: [ function (to, from) { if (to.query.invite !== 'yes') { return navigateTo('/') } }, ], }) </script>

选择依据:跨全站的政策用全局,跨多页的角色规则用命名,单页特例用内联。全局中间件每个请求都会跑,里面放重逻辑(比如远程查权限)会拖慢所有导航。

登录守卫的完整实现

把三级体系组装成可用的鉴权流。会话状态用 cookie 承载——cookie 会随 SSR 请求自动到达服务器,这是它在交接班模型里的独特优势(useState 只在 payload 里,首航时服务器读不到"游客想干什么"之外的信息;而 cookie 是浏览器主动递上的证件):

// composables/useAuth.ts // 会话三件套:读状态、登录、登出 export function useAuth() { const token = useCookie<string | null>('session-token', { maxAge: 60 * 60 * 24 * 7, // 一周有效 sameSite: 'lax', }) const isLoggedIn = computed(() => Boolean(token.value)) async function login(credentials: { email: string; password: string }) { const res = await $fetch('/api/auth/login', { method: 'POST', body: credentials }) token.value = res.token // 写 cookie:两端可见,服务器可验 } function logout() { token.value = null return navigateTo('/') } return { token, isLoggedIn, login, logout } }
// middleware/auth.global.ts:守卫规则与上面的会话配合 export default defineRouteMiddleware((to) => { const { isLoggedIn } = useAuth() const publicPages = ['/', '/login', '/about', '/products'] const isPublic = publicPages.includes(to.path) if (!isLoggedIn.value && !isPublic) { return navigateTo({ path: '/login', query: { redirect: to.fullPath } }) } // 已登录就别再看登录页 if (isLoggedIn.value && to.path === '/login') { return navigateTo('/account') } })

登录页消费回跳参数:

<!-- pages/login.vue --> <script setup> const route = useRoute() const { login } = useAuth() const busy = ref(false) async function submit(form: { email: string; password: string }) { busy.value = true try { await login(form) // 登录成功:回跳到守卫记录的地址,没有则去账户页 await navigateTo((route.query.redirect as string) || '/account') } finally { busy.value = false } } </script>

两端执行的细节

路由中间件的特别之处:首航在服务端执行,后续导航在客户端执行。对照管线图(图 3-1 的第④步):浏览器直接访问受保护页时,SSR 管线里中间件先跑,未登录直接在服务端返回 302 重定向,HTML 都不用渲染——省一次完整渲染。客户端换页时,中间件在浏览器里跑,同样逻辑不用改。

图 5-2:两类中间件的拦截位置

图 5-2:两类中间件的拦截位置

两端都跑带来一条纪律:中间件里不能有单端 API。window 判断放进 import.meta.client 分支;需要查库验权限时,中间件里调 API 端点(走 $fetch 到第 6 章的 server/api),而不是直接 import 服务端模块——那会把它拖进客户端 bundle。

// middleware/verify.global.ts:需要服务端裁决时的姿势 export default defineRouteMiddleware(async () => { // 敏感区域的权限让服务器说了算(接口内部查会话库) const ok = await $fetch('/api/auth/verify', { params: { area: 'billing' } }) if (!ok.allowed) { return navigateTo('/account') } })

⚠️ 常见坑:全局中间件里放接口请求且不带缓存。每次导航一个请求,页面切快了请求排队,导航肉眼可见变卡。验证类接口要短缓存或降频,纯 UI 判断用本地状态。

💡 关键直觉:中间件是导航的"边检"——首航在出境口岸(服务端)查一次,换页在境内卡口(浏览器)查。规则写在同一份代码里,两端各自执行,这就是交接班模型里罕见的"双向通用逻辑"。

本节要点回顾

  • 三级体系.global 全站执行、命名按页声明、内联单页一次性,按影响范围选级;
  • 会话放 cookie:useCookie 两端可读写且随请求自动抵服务器,是鉴权状态的正确容器;
  • 守卫三段式:白名单 + 未登录改道带 redirect、已登录挡登录页、登录后回跳;
  • 两端执行:首航服务端跑(可省渲染直接 302),换页浏览器跑,规则一份两端通用;
  • 服务端裁决走 API:中间件内不 import 服务端模块,验权请求发给 server 端点。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U