vue中動態(tài)權限控制的實現(xiàn)示例
一、核心原理與流程總覽
動態(tài)權限控制的本質是:用戶登錄后,從后端獲取其權限數(shù)據(jù),前端根據(jù)此數(shù)據(jù)動態(tài)地構建出只屬于該用戶的可訪問路由和菜單,并在視圖層面(按鈕)進行權限控制。
整個流程可以分為以下幾個核心步驟,下圖清晰地展示了其工作原理和閉環(huán)流程:
flowchart TD
A[用戶登錄] --> B[獲取用戶權限數(shù)據(jù)JSON]
B -- 解析為前端所需結構 --> C[生成動態(tài)路由]
C -- addRoute添加到路由器 --> D[路由器Router]
D -- 根據(jù)當前路由生成 --> E[側邊欄菜單
(動態(tài)菜單組件)]
E -- 點擊菜單項觸發(fā)路由切換 --> D
F[訪問路由] --> G{路由守衛(wèi)檢查權限}
G -- 有權限 --> H[正常渲染組件]
G -- 無權限 --> I[跳轉404或登錄頁]
H -- 組件內按鈕 --> J{按鈕權限指令v-permission}
J -- 權限碼匹配 --> K[顯示按鈕]
J -- 權限碼不匹配 --> L[移除按鈕DOM]
下面,我們將按照這個流程中的每一個環(huán)節(jié),進行詳細的原理說明和代碼實現(xiàn)。
二、詳細步驟與代碼實現(xiàn)
步驟 1: 定義權限數(shù)據(jù)結構與狀態(tài)管理
首先,我們需要在后端和前端約定好權限數(shù)據(jù)的結構。
1.1 后端返回的權限數(shù)據(jù)示例 (GET /api/user/permissions):
通常,后端會返回一個樹形結構,包含前端定義的路由和權限點。
{
"code": 200,
"data": {
"userInfo": { "name": "Alice", "avatar": "" },
"permissions": [
{
"id": 1,
"parentId": 0,
"path": "/system",
"name": "System",
"meta": { "title": "系統(tǒng)管理", "icon": "setting", "requiresAuth": true },
"children": [
{
"id": 2,
"parentId": 1,
"path": "user",
"name": "UserManagement",
"meta": { "title": "用戶管理", "requiresAuth": true },
"btnPermissions": ["user:add", "user:edit", "user:delete"] // 按鈕級權限標識
}
]
},
{
"id": 3,
"parentId": 0,
"path": "/about",
"name": "About",
"meta": { "title": "關于", "icon": "info", "requiresAuth": false }
}
]
}
}
1.2 前端定義靜態(tài)路由和動態(tài)路由
我們將路由分為兩類:
- 靜態(tài)路由 (Constant Routes): 無需權限即可訪問的路由,如
/login,/404。 - 動態(tài)路由 (Dynamic Routes / Async Routes): 需要根據(jù)權限動態(tài)添加的路由。
/src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import { useUserStore } from '@/stores/user';
// 靜態(tài)路由
export const constantRoutes = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
meta: { title: '登錄', hidden: true } // hidden 表示不在側邊欄顯示
},
{
path: '/404',
name: 'NotFound',
component: () => import('@/views/404.vue'),
meta: { title: '404', hidden: true }
}
];
// 動態(tài)路由(初始化為空,后續(xù)根據(jù)權限添加)
// 注意:這里不是直接定義,而是提供一個和后臺數(shù)據(jù)匹配的模板
export const asyncRoutesMap = {
'UserManagement': {
path: 'user', // 會拼接到父路由的 path 上
name: 'UserManagement',
component: () => import('@/views/system/UserManagement.vue'), // 需要提前創(chuàng)建好組件
meta: { title: '用戶管理', requiresAuth: true }
},
'RoleManagement': {
path: 'role',
name: 'RoleManagement',
component: () => import('@/views/system/RoleManagement.vue'),
meta: { title: '角色管理', requiresAuth: true }
}
// ... 其他所有可能的路由
};
const router = createRouter({
history: createWebHistory(),
routes: constantRoutes // 初始化時只掛載靜態(tài)路由
});
export default router;
1.3 使用 Pinia 存儲權限狀態(tài)/src/stores/user.js
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { getPermission } from '@/api/user';
import { asyncRoutesMap } from '@/router';
import { generateRoutes, generateMenu } from '@/utils/permission';
export const useUserStore = defineStore('user', () => {
const token = ref('');
const userInfo = ref({});
const permissions = ref([]); // 存儲原始權限數(shù)據(jù)
const dynamicRoutes = ref([]); // 存儲生成后的動態(tài)路由對象
const menus = ref([]); // 存儲用于生成導航菜單的數(shù)據(jù)
// 獲取用戶權限信息
const getUserPermissions = async () => {
try {
const res = await getPermission();
permissions.value = res.data.permissions;
userInfo.value = res.data.userInfo;
// 核心:根據(jù)權限數(shù)據(jù)生成動態(tài)路由和菜單
const { routes, menuList } = generateRoutesAndMenus(permissions.value, asyncRoutesMap);
dynamicRoutes.value = routes;
menus.value = menuList;
return dynamicRoutes.value;
} catch (error) {
console.error('獲取權限失敗', error);
return [];
}
};
// 退出登錄清空狀態(tài)
const logout = () => {
token.value = '';
userInfo.value = {};
permissions.value = [];
dynamicRoutes.value = [];
menus.value = [];
};
return {
token,
userInfo,
permissions,
dynamicRoutes,
menus,
getUserPermissions,
logout
};
});
// 工具函數(shù):遞歸處理權限數(shù)據(jù),生成路由和菜單
export const generateRoutesAndMenus = (permissionList, routeMap) => {
const routes = [];
const menuList = [];
const traverse = (nodes, isChild = false) => {
nodes.forEach(node => {
// 1. 生成菜單項
const menuItem = {
path: node.path,
name: node.name,
meta: { ...node.meta, btnPermissions: node.btnPermissions }, // 保存按鈕權限
children: []
};
if (isChild) {
menuList[menuList.length - 1]?.children.push(menuItem);
} else {
menuList.push(menuItem);
}
// 2. 生成路由項 (只處理有 component 的節(jié)點,即葉子節(jié)點或需要布局的節(jié)點)
// 如果后端返回的節(jié)點名稱能在我們的映射表 asyncRoutesMap 中找到,說明是有效路由
if (routeMap[node.name]) {
const route = {
...routeMap[node.name], // 展開映射表中的預設配置(最重要的是component)
path: node.path,
name: node.name,
meta: { ...node.meta, btnPermissions: node.btnPermissions }
};
routes.push(route);
}
// 3. 遞歸處理子節(jié)點
if (node.children && node.children.length > 0) {
traverse(node.children, true);
}
});
};
traverse(permissionList);
return { routes, menuList };
};
步驟 2: 登錄與獲取權限數(shù)據(jù)
/src/views/Login.vue
<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useUserStore } from '@/stores/user';
const router = useRouter();
const userStore = useUserStore();
const loginForm = ref({ username: '', password: '' });
const handleLogin = async () => {
try {
// 1. 執(zhí)行登錄請求,獲取 token
const loginRes = await api.login(loginForm.value);
userStore.token = loginRes.data.token;
// 2. 獲取用戶權限信息
const dynamicRoutes = await userStore.getUserPermissions();
// 3. 動態(tài)添加路由
dynamicRoutes.forEach(route => {
// 注意:addRoute 可以接受父路由的 name 作為第一個參數(shù),來實現(xiàn)嵌套路由的添加
// 這里假設我們的權限數(shù)據(jù)已經(jīng)是一個平鋪的數(shù)組,或者使用其他方式匹配父路由
// 一種更復雜的實現(xiàn)需要遞歸處理嵌套路由的添加,這里簡化演示
router.addRoute(route); // 添加到根路由
// 如果路由有父級,例如:router.addRoute('ParentRouteName', route);
});
// 4. 添加一個兜底的 404 路由(必須放在最后)
router.addRoute({
path: '/:pathMatch(.*)*',
name: 'CatchAll',
redirect: '/404'
});
// 5. 跳轉到首頁
router.push('/');
} catch (error) {
console.error('登錄失敗', error);
}
};
</script>
步驟 3: 路由守衛(wèi)進行權限校驗
/src/router/index.js (在原有代碼上追加)
// ... 之前的導入和路由初始化代碼 ...
// 路由守衛(wèi)
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore();
const token = userStore.token;
// 1. 判斷是否有 token
if (token) {
// 2. 如果是訪問登錄頁,直接跳轉到首頁
if (to.path === '/login') {
next('/');
} else {
// 3. 判斷是否已經(jīng)拉取過用戶權限信息
if (userStore.permissions.length === 0) {
try {
// 4. 如果沒有獲取權限,則獲取權限并添加動態(tài)路由
const dynamicRoutes = await userStore.getUserPermissions();
dynamicRoutes.forEach(route => {
router.addRoute(route);
});
// 5. 添加完動態(tài)路由后,需要重定向到目標路由 to
// replace: true 防止重復添加路由導致導航失敗
next({ ...to, replace: true });
} catch (error) {
// 6. 如果獲取失敗,可能是 token 過期,清除狀態(tài)并跳回登錄頁
userStore.logout();
next(`/login?redirect=${to.path}`);
}
} else {
// 7. 如果已經(jīng)有權限信息,直接放行
next();
}
}
} else {
// 8. 沒有 token
if (to.meta.requiresAuth === false || to.path === '/login') {
// 如果目標路由不需要權限或者是登錄頁,則放行
next();
} else {
// 否則,跳轉到登錄頁,并記錄重定向地址
next(`/login?redirect=${to.path}`);
}
}
});
步驟 4: 根據(jù)權限數(shù)據(jù)生成動態(tài)菜單
使用上面 Pinia 中生成的 menus 來循環(huán)生成側邊欄菜單。
/src/components/Layout/Sidebar.vue
<template>
<el-menu
:default-active="$route.path"
router
unique-opened
background-color="#304156"
text-color="#bfcbd9"
active-text-color="#409EFF"
>
<sidebar-item
v-for="menu in userStore.menus"
:key="menu.path"
:item="menu"
/>
</el-menu>
</template>
<script setup>
import SidebarItem from './SidebarItem.vue';
import { useUserStore } from '@/stores/user';
const userStore = useUserStore();
</script>
/src/components/Layout/SidebarItem.vue (遞歸組件)
<template>
<!--- 如果有子菜單,渲染 el-sub-menu -->
<el-sub-menu
v-if="item.children && item.children.length > 0"
:index="item.path"
>
<template #title>
<el-icon><component :is="item.meta.icon" /></el-icon>
<span>{{ item.meta.title }}</span>
</template>
<sidebar-item
v-for="child in item.children"
:key="child.path"
:item="child"
/>
</el-sub-menu>
<!--- 如果沒有子菜單,渲染 el-menu-item -->
<el-menu-item v-else :index="resolvePath(item.path)">
<el-icon><component :is="item.meta.icon" /></el-icon>
<template #title>{{ item.meta.title }}</template>
</el-menu-item>
</template>
<script setup>
import { resolve } from 'path-browserify';
const props = defineProps({
item: {
type: Object,
required: true
},
basePath: {
type: String,
default: ''
}
});
// 處理完整路徑(如果需要處理嵌套路徑)
function resolvePath(routePath) {
return resolve(props.basePath, routePath);
}
</script>
步驟 5: 實現(xiàn)按鈕級權限控制
有兩種常見方式:自定義指令 和 函數(shù)組件。這里展示更優(yōu)雅的自定義指令方式。
5.1 創(chuàng)建權限指令 v-permission/src/directives/permission.js
import { useUserStore } from '@/stores/user';
// 按鈕權限檢查函數(shù)
function checkPermission(el, binding) {
const { value } = binding; // 指令的綁定值,例如 v-permission="'user:add'"
const userStore = useUserStore();
const btnPermissions = userStore.currentRouteBtnPermissions; // 需要從當前路由元信息中獲取按鈕權限
// 從當前路由的 meta 中獲取按鈕權限列表
// 注意:需要在路由守衛(wèi)或菜單生成時,將 btnPermissions 存儲到當前路由的 meta 中
// 這里假設我們已經(jīng)有了 currentRouteBtnPermissions
if (value && Array.isArray(btnPermissions)) {
const hasPermission = btnPermissions.includes(value);
if (!hasPermission) {
// 如果沒有權限,則移除該元素
el.parentNode && el.parentNode.removeChild(el);
}
} else {
throw new Error(`需要指定權限標識,如 v-permission="'user:add'"`);
}
}
export default {
mounted(el, binding) {
checkPermission(el, binding);
},
updated(el, binding) {
checkPermission(el, binding);
}
};
/src/main.js
// ...
import permissionDirective from '@/directives/permission';
const app = createApp(App);
app.directive('permission', permissionDirective);
// ...
5.2 在 Pinia 中提供獲取當前路由按鈕權限的方法
修改 /src/stores/user.js
import { useRoute } from 'vue-router';
// ...
export const useUserStore = defineStore('user', () => {
// ... 其他狀態(tài) ...
// 計算屬性:獲取當前路由的按鈕權限
const currentRouteBtnPermissions = computed(() => {
const route = useRoute();
return route.meta.btnPermissions || []; // 從當前路由的元信息中獲取
});
return {
// ... 其他返回 ...
currentRouteBtnPermissions
};
});
5.3 在組件中使用指令/src/views/system/UserManagement.vue
<template>
<div>
<el-button
type="primary"
v-permission="'user:add'"
@click="handleAdd"
>新增用戶</el-button>
<el-button
type="warning"
v-permission="'user:edit'"
@click="handleEdit"
>編輯</el-button>
<el-button
type="danger"
v-permission="'user:delete'"
@click="handleDelete"
>刪除</el-button>
<el-table :data="tableData">
<!-- ... -->
</el-table>
</div>
</template>
三、注意事項與優(yōu)化
- 路由組件加載: 確保
component: () => import(...)中的路徑正確,Webpack/Vite 會將這些組件打包到獨立的 chunk 中實現(xiàn)懶加載。 - 404 路由處理: 動態(tài)添加路由后,一定要確保
router.addRoute({ path: '/:pathMatch(.*)*', redirect: '/404' })是最后一個添加的路由。 - 按鈕權限的存儲: 上述指令示例中,按鈕權限是從當前路由的
meta中獲取。你需要確保在路由導航守衛(wèi)或生成動態(tài)路由時,將每個路由對應的btnPermissions正確地設置到其meta中。 - 權限更新: 如果系統(tǒng)支持用戶動態(tài)更改權限(如切換角色),需要在權限變更后調用
router.go(0)刷新頁面或手動重置路由狀態(tài)。 - 安全性: 前端權限控制只是為了用戶體驗和基礎防護,真正的權限校驗必須在后端 API 層面嚴格執(zhí)行。
到此這篇關于vue中動態(tài)權限控制的實現(xiàn)示例的文章就介紹到這了,更多相關vue 動態(tài)權限控制內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
element-ui?tree?手動展開功能實現(xiàn)(異步樹也可以)
這篇文章主要介紹了element-ui?tree?手動進行展開(異步樹也可以),項目中用到了vue的element-ui框架,用到了el-tree組件,需要的朋友可以參考下2022-08-08
在導入.vue文件的時候,ts報錯提示:找不到模塊“@/Layout/index.vue”或其相應的類型聲明問題
這篇文章主要介紹了在導入.vue文件的時候,ts報錯提示:找不到模塊“@/Layout/index.vue”或其相應的類型聲明問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-08-08

