エンタープライズレベルのアプリケーションを開発する場合、権限制御は決して単純な「ログイン後のジャンプ」ではありません。通常、次の 3 つの側面における複雑な訴えが含まれています。
これら 3 つの制御層を実現するために、従来の「フロントエンドでハードコーディングされた完全なルーティング」アプローチを放棄し、RBAC (役割ベースのアクセス制御) モデルに基づくフロントエンドとバックエンドの分離動的ルーティング ソリューション。
Vue Router 4 は非常に強力な機能を提供します addRoute API。我们的核心设计思想是:静态路由包含基础页面,动态路由由后端下发。
为了配合前端渲染,后端需要返回两类数据:菜单树(用于渲染侧边栏) 和 权限编码列表(用于按钮级控制)。
// 后端返回的权限数据格式 interface PermissionResponse
3. コア実装 1: グローバル ルーター ガードと動的登録
新しい手書きルーティング ガードの多くは、ページが更新されると動的ルーティングが失われるという 2 つの致命的なバグを最も起こしやすいです。 無限ループのリダイレクト。
私たちのソリューションは次のとおりです guards.ts 以下の点で究極のステータス管理を実現します。
// src/router/guards.ts
import type menu.name,
icon: menu.meta?.icon,
permission: menu.meta?.permission
from 'vue-router'
import from '@/stores/user'
import from '@/stores/permission'
export function setupRouterGuards(router: Router) {
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore()
const permissionStore = usePermissionStore()
// 1. 白名单放行(无需登录的页面)
if (to.meta.requiresAuth === false) {
return next()
}
// 2. 判断是否已登录(是否有 Token)
if (!userStore.token) {
return next({ path: " query: { redirect: to.fullPath } })
}
// 3. 核心:判断是否已拉取过权限
// 第一次进入,或者刷新页面时,permissionStore.loaded 为 false
if (!permissionStore.loaded) {
try {
// 拉取用户信息和权限路由,并动态 addRoute
await permissionStore.fetchPermissions()
// 关键操作:路由注册完成后,必须使用 replace 重新进入当前路由
// 否则会出现白屏或者 404
return next({ ...to, replace: true })
} catch (error) {
// 如果获取权限失败(如 Token 过期),清除凭证并踢回登录页
userStore.clearToken()
return next("
}
}
// 4. 按钮级权限的细粒度路由校验(可选)
const requiredPermission = to.meta?.permission as string
if (requiredPermission && !permissionStore.hasPermission(requiredPermission)) {
return next('/403')
}
next()
})
}
キーポイント: なぜルートだけではだめなのか next() 過去?
初めてアクセスしたときはルートがまだ確立されていないため、 addRoute 現時点で直接完了 next() それでも 404 と一致します。 next({ ...to, replace: true }) 現在のナビゲーションを中断し、登録が完了するまで待ってから、新しいルーティング テーブルに基づいてナビゲーションを再開できます。
ピット踏みつけ記録1:存在 next() 崩壊の午後
動的ルーティングが初めて導入されたとき、ローカル開発は非常にスムーズでした。テスト環境に置くとすぐに、ページを更新するとすぐにブラウザがフリーズし、さらにプロンプトが表示されました。 Maximum call stack size exceeded(スタックオーバーフロー)。
開けてみた router.beforeEach ログを見てみると、 login そして dashboard 激しく何度も飛び跳ねる。
原因分析:
ページを更新すると、permissionStore.loaded イニシャルは false。ルーティングが遮断された後、インターフェイスをプルし、プルを終了します。 loaded に設定 true、それから next() リリース。しかし、私は論理の致命的な部分を見逃していました。この時だったら token 期限が切れると、インターフェイスは 401 を返します。
私の Axios インターセプターは 401 をキャプチャし、トークンをクリアしてジャンプします /login。このとき、ルーティング ガードが再びトリガーされ、トークンが存在しないことがわかり、再びホップをインターセプトしました。 /login;ただしルートマッチングの混乱のため、または next({...to}) が間違って書かれていたため、行き止まりで激しく回転してしまいました。
本当の解決策:
ガードでは許可を引くアクションを追加する必要があります try...catch そして勢いよく飛び出して協力する。
if (!permissionStore.loaded) {
try {
await permissionStore.fetchPermissions()
// 坑点:千万别写 next(),必须重新触发一次导航,让新挂载的路由生效
return next({ ...to, replace: true })
} catch (err) {
// 坑点:这里如果不手动清理 token,死循环绝对跑不掉
userStore.clearToken()
return next({ path: " })
}
}
4. コア実装 2: Pinia 状態管理と動的ルーティング分析
存在する 許可.ts 中,我们需要处理后端传来的菜单树,将其转换为 Vue Router 能够识别的ルートレコード生格式。
// src/stores/permission.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
import router from '@/router'
import type { RouteRecordRaw } from 'vue-router'
export const usePermissionStore = defineStore('permission', () => {
// 状态
const loaded = ref(false)
const permissions = ref([])
const dynamicRoutes = ref([])
// 动态导入组件的黑魔法
const modules = import.meta.glob('@/views/**/*.vue')
// 将后端菜单转换为 Vue Router 格式
const transformMenuToRoute = (menus: any[]): RouteRecordRaw[] => {
return menus.map(menu => {
const route: RouteRecordRaw = {
path: menu.path,
name: menu.name,
// 核心:通过 glob 映射,避免后端返回字符串导致无法加载组件
component: modules[`/src/views/${menu.component}.vue`],
meta: {
title: menu.meta?.title || menu.name,
icon: menu.meta?.icon,
permission: menu.meta?.permission
},
children: menu.children ? transformMenuToRoute(menu.children) : []
}
return route
})
}
// 拉取权限并动态挂载
const fetchPermissions = async () => {
// 模拟 API 请求
const res = await api.getPermissions()
permissions.value = res.permissions
dynamicRoutes.value = transformMenuToRoute(res.menus)
// 关键:将解析后的路由,动态添加到主 Layout 路由下
dynamicRoutes.value.forEach(route => {
router.addRoute('Layout', route)
})
loaded.value = true
}
const hasPermission = (code: string) => {
return permissions.value.includes(code)
}
return { loaded, permissions, dynamicRoutes, fetchPermissions, hasPermission }
})
重要なポイント:バックエンドによって返されました component 文字列のみにすることができます (例: "system/user/index")。絶対に使用しないでください import(menu.component) この種の動的な文字列連結は、Vite がパッケージ化するときに変数として処理するため、コンポーネントが失われます。必ず使用する import.meta.glob すべてのビュー ファイルをプリロードします。
蹂躙記録2:Vite Dynamic Importの「行き止まり」
動的ルーティング解析を作成した直後に、私は興奮して新しいメニュー テストを追加しました。結果をクリックして更新するとすぐにページが空白になり、コンソールにエラーが報告されます。Failed to fetch dynamically imported module: /src/views/system/user/index.vue。
トラブルシューティングに 4 時間を費やしましたが、Vite のバグかと思うところでした。後で、Vue Router のネイティブであることを発見しました。 component: () => import(XXX) 静的文字列のみをサポートします。バックエンドが戻るため menu.component 変数です(例: 'system/user/index')、Vite はパッケージ化時にこの変数がどのファイルを指しているかを分析できず、その結果、このコンポーネントはパッケージ化後にまったく含まれなくなります。
本当の解決策(キラーバージョン):
Vite が提供するものを使用する必要があります import.meta.glob。しかし、非常に陥りやすい別の落とし穴があります。glob パスの接合。 Windows システムの場合、パスはバックスラッシュになります。私の最終的なコードは次のとおりです。
// 解决前: modules[`/src/views/${menu.component}.vue`] 容易因为大小写或斜杠报错
// 解决后:加入容错处理,并在路径中强制替换
const modules = import.meta.glob('../../views/**/*.vue')
const getComponent = (componentPath: string) => {
const path = `../../views/${componentPath}.vue`
// 如果找不到,就给个 404 占位组件,防止整个应用崩溃
return modules[path] || (() => import('../../views/error/404.vue'))
}
(その時は一つ少なく書いたので ../../私は画面に目がくらんでしまいました。 )
5. コア実装 3: ボタンレベルの許可ディレクティブ (カスタマイズされたディレクティブ)
ボタンレベルの権限制御の場合、使用する場合 v-if="hasPermission('xxx')"ページのコードは非常に醜くなり、保守が困難になります。
カスタム命令をカプセル化します v-permission エレガントなコントロールを実現します。このコマンドはマウント時に判断するだけでなく、アクセス許可が変更されたときにも応答する必要があります (これはめったに起こりません)。
// src/directives/permission.ts
import { type Directive, type DirectiveBinding } from 'vue'
import { usePermissionStore } from '@/stores/permission'
export const permission: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
const { value } = binding
const permissionStore = usePermissionStore()
// 支持传入数组,如 v-permission="['user:add', 'user:edit']" (满足其一即可)
const requiredPermissions = Array.isArray(value) ? value : [value]
if (requiredPermissions.length > 0) {
const hasPerm = requiredPermissions.some(perm => permissionStore.hasPermission(perm))
if (!hasPerm) {
// 如果没有权限,直接从 DOM 树中移除该元素
el.parentNode?.removeChild(el)
}
} else {
throw new Error('v-permission 指令需要指定权限编码,如 v-permission="\'user:add\'"')
}
}
}
メインエントランスにて(main.ts) このディレクティブを登録するには:
app.directive('permission', permission)
ビジネスコードでは、使用法は非常に簡潔です。
悲劇の記録 3: フロントエンドの指示だけでは不十分
書き終えました v-permission 説明の後、私は興奮してデモを行ってみました。管理者には「削除」ボタンが表示されますが、一般のテスターがログインすると、ボタンは表示されなくなります。
しかし、私のフロントエンド リーダー (または反省) は、F12 開発者ツールを直接開き、要素検査で、 ラベルが手動で元に戻されたか、削除インターフェイスがコンソールで直接呼び出されました。 fetch('/api/v1/delete')データはまだ削除できます。
原因分析:v-permission これは単なる隠蔽であり、ツリーから DOM 要素を取り出しているだけですが、最新のブラウザーの SPA (シングル ページ アプリケーション) を使用すると、ユーザーはメモリと DOM を簡単に変更できます。
本当の解決策:
フロントエンドの権限ディレクティブは単なるユーザー エクスペリエンス (UX) の最適化です。実際のセキュリティ傍受はバックエンドで行う必要があります (つまり、以前に投稿された RBAC 記事内) Depends)。
ただし、ここでの手順を最適化しました。ユーザーの誤操作やエラーの報告を防ぐために、次のように追加しました。 display: none の代わりに removeChild。
// 优化后的指令挂载
mounted(el: HTMLElement, binding: DirectiveBinding) {
const { value } = binding
const permissionStore = usePermissionStore()
const requiredPermissions = Array.isArray(value) ? value : [value]
const hasPerm = requiredPermissions.some(perm => permissionStore.hasPermission(perm))
if (!hasPerm) {
// 不用 removeChild,直接隐藏,避免 Vue 在 Diff 时找不到节点报错
el.style.display = 'none'
}
}
6. パフォーマンスの最適化と極限シーンの分析
-
複数のタブ ページ (タブ) にルートを繰り返し追加します。:
ユーザーが更新をクリックするか、それを複数回呼び出した場合fetchPermissions、router.addRoute繰り返し追加される場合があります。
解決:存在するpermission.tsで紹介されたrouter.hasRoute(name)判断するか、利用するかrouter.removeRoute(name)古いルートをクリーンアップします。 -
401/403 ステータス コードの傍受:
フロントエンド Axios インターセプターはルート ガードにリンクされている必要があります。インターフェイスが 401 を返したら、それをクリアしますuserStore.tokenそしてpermissionStore.loadedそして使用しますwindow.location.href = "ページを強制的に更新して、フロントエンドの残りの状態を完全に破棄します。 -
ディープコピーによって引き起こされるパフォーマンスの問題:
メニュー ツリーを再帰的に変換する場合は、バックエンドから返された元のデータを直接変更しないように、新しいオブジェクトを生成する方法を使用するようにしてください。
7. まとめ
フロントエンドのアクセス許可制御は、単なる数行のコードではなく、フロントエンド アーキテクチャの機能を包括的に考慮したものです。エレガントな許可システムは、「処女のように静か (静的ルーティング)、ウサギのように動的 (動的ルーティングの読み込み)、シームレスな切り替え (状態を失わずにシームレスにリフレッシュ)」という特性を備えている必要があります。
上記のソリューションは、独自に開発したフルスタック インテリジェント テスト プラットフォーム (AITest V1.0) で安定して実行されており、数百のメニュー ノードと数十のロール割り当てを処理する場合でも、依然としてスムーズに動作します。この詳細な実践が、高度な Vue3 への道での権限制御の落とし穴を回避するのに役立つことを願っています。
**私はジェイソン、独立系開発者です。フルスタック開発とアーキテクチャ設計、および AI インテリジェントな実装に焦点を当てます。私の個人ホームページにAITest実践レビューを連載しています。いつでも注意を払い、コミュニケーションをとることを歓迎します。 **