当前位置: 首页 > news >正文

一文学会vue的动态权限控制

在这里插入图片描述

一、核心原理与流程总览

动态权限控制的本质是:用户登录后,从后端获取其权限数据,前端根据此数据动态地构建出只属于该用户的可访问路由和菜单,并在视图层面(按钮)进行权限控制。

整个流程可以分为以下几个核心步骤,下图清晰地展示了其工作原理和闭环流程:

flowchart TDA[用户登录] --> B[获取用户权限数据JSON]B -- 解析为前端所需结构 --> C[生成动态路由]C -- addRoute添加到路由器 --> D[路由器Router]D -- 根据当前路由生成 --> E[侧边栏菜单<br>(动态菜单组件)]E -- 点击菜单项触发路由切换 --> DF[访问路由] --> G{路由守卫检查权限}G -- 有权限 --> H[正常渲染组件]G -- 无权限 --> I[跳转404或登录页]H -- 组件内按钮 --> J{按钮权限指令v-permission}J -- 权限码匹配 --> K[显示按钮]J -- 权限码不匹配 --> L[移除按钮DOM]

下面,我们将按照这个流程中的每一个环节,进行详细的原理说明和代码实现。


二、详细步骤与代码实现

步骤 1: 定义权限数据结构与状态管理

首先,我们需要在后端和前端约定好权限数据的结构。

1.1 后端返回的权限数据示例 (GET /api/user/permissions):
通常,后端会返回一个树形结构,包含前端定义的路由和权限点。

{"code": 200,"data": {"userInfo": { "name": "Alice", "avatar": "" },"permissions": [{"id": 1,"parentId": 0,"path": "/system","name": "System","meta": { "title": "系统管理", "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 前端定义静态路由和动态路由
我们将路由分为两类:

  • 静态路由 (Constant Routes): 无需权限即可访问的路由,如 /login, /404
  • 动态路由 (Dynamic Routes / Async Routes): 需要根据权限动态添加的路由。

/src/router/index.js

import { createRouter, createWebHistory } from 'vue-router';
import { useUserStore } from '@/stores/user';// 静态路由
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 }}
];// 动态路由(初始化为空,后续根据权限添加)
// 注意:这里不是直接定义,而是提供一个和后台数据匹配的模板
export const asyncRoutesMap = {'UserManagement': {path: 'user', // 会拼接到父路由的 path 上name: 'UserManagement',component: () => import('@/views/system/UserManagement.vue'), // 需要提前创建好组件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 // 初始化时只挂载静态路由
});export default router;

1.3 使用 Pinia 存储权限状态
/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([]); // 存储原始权限数据const dynamicRoutes = ref([]); // 存储生成后的动态路由对象const menus = ref([]); // 存储用于生成导航菜单的数据// 获取用户权限信息const getUserPermissions = async () => {try {const res = await getPermission();permissions.value = res.data.permissions;userInfo.value = res.data.userInfo;// 核心:根据权限数据生成动态路由和菜单const { routes, menuList } = generateRoutesAndMenus(permissions.value, asyncRoutesMap);dynamicRoutes.value = routes;menus.value = menuList;return dynamicRoutes.value;} catch (error) {console.error('获取权限失败', error);return [];}};// 退出登录清空状态const logout = () => {token.value = '';userInfo.value = {};permissions.value = [];dynamicRoutes.value = [];menus.value = [];};return {token,userInfo,permissions,dynamicRoutes,menus,getUserPermissions,logout};
});// 工具函数:递归处理权限数据,生成路由和菜单
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 的节点,即叶子节点或需要布局的节点)// 如果后端返回的节点名称能在我们的映射表 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. 递归处理子节点if (node.children && node.children.length > 0) {traverse(node.children, true);}});};traverse(permissionList);return { routes, menuList };
};

步骤 2: 登录与获取权限数据

/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. 执行登录请求,获取 tokenconst loginRes = await api.login(loginForm.value);userStore.token = loginRes.data.token;// 2. 获取用户权限信息const dynamicRoutes = await userStore.getUserPermissions();// 3. 动态添加路由dynamicRoutes.forEach(route => {// 注意:addRoute 可以接受父路由的 name 作为第一个参数,来实现嵌套路由的添加// 这里假设我们的权限数据已经是一个平铺的数组,或者使用其他方式匹配父路由// 一种更复杂的实现需要递归处理嵌套路由的添加,这里简化演示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: 路由守卫进行权限校验

/src/router/index.js (在原有代码上追加)

// ... 之前的导入和路由初始化代码 ...// 路由守卫
router.beforeEach(async (to, from, next) => {const userStore = useUserStore();const token = userStore.token;// 1. 判断是否有 tokenif (token) {// 2. 如果是访问登录页,直接跳转到首页if (to.path === '/login') {next('/');} else {// 3. 判断是否已经拉取过用户权限信息if (userStore.permissions.length === 0) {try {// 4. 如果没有获取权限,则获取权限并添加动态路由const dynamicRoutes = await userStore.getUserPermissions();dynamicRoutes.forEach(route => {router.addRoute(route);});// 5. 添加完动态路由后,需要重定向到目标路由 to// replace: true 防止重复添加路由导致导航失败next({ ...to, replace: true });} catch (error) {// 6. 如果获取失败,可能是 token 过期,清除状态并跳回登录页userStore.logout();next(`/login?redirect=${to.path}`);}} else {// 7. 如果已经有权限信息,直接放行next();}}} else {// 8. 没有 tokenif (to.meta.requiresAuth === false || to.path === '/login') {// 如果目标路由不需要权限或者是登录页,则放行next();} else {// 否则,跳转到登录页,并记录重定向地址next(`/login?redirect=${to.path}`);}}
});

步骤 4: 根据权限数据生成动态菜单

使用上面 Pinia 中生成的 menus 来循环生成侧边栏菜单。

/src/components/Layout/Sidebar.vue

<template><el-menu:default-active="$route.path"routerunique-openedbackground-color="#304156"text-color="#bfcbd9"active-text-color="#409EFF"><sidebar-itemv-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-menuv-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-itemv-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: 实现按钮级权限控制

有两种常见方式:自定义指令函数组件。这里展示更优雅的自定义指令方式。

5.1 创建权限指令 v-permission
/src/directives/permission.js

import { useUserStore } from '@/stores/user';// 按钮权限检查函数
function checkPermission(el, binding) {const { value } = binding; // 指令的绑定值,例如 v-permission="'user:add'"const userStore = useUserStore();const btnPermissions = userStore.currentRouteBtnPermissions; // 需要从当前路由元信息中获取按钮权限// 从当前路由的 meta 中获取按钮权限列表// 注意:需要在路由守卫或菜单生成时,将 btnPermissions 存储到当前路由的 meta 中// 这里假设我们已经有了 currentRouteBtnPermissionsif (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', () => {// ... 其他状态 ...// 计算属性:获取当前路由的按钮权限const currentRouteBtnPermissions = computed(() => {const route = useRoute();return route.meta.btnPermissions || []; // 从当前路由的元信息中获取});return {// ... 其他返回 ...currentRouteBtnPermissions};
});

5.3 在组件中使用指令
/src/views/system/UserManagement.vue

<template><div><el-buttontype="primary"v-permission="'user:add'"@click="handleAdd">新增用户</el-button><el-buttontype="warning"v-permission="'user:edit'"@click="handleEdit">编辑</el-button><el-buttontype="danger"v-permission="'user:delete'"@click="handleDelete">删除</el-button><el-table :data="tableData"><!-- ... --></el-table></div>
</template>

三、注意事项与优化

  1. 路由组件加载: 确保 component: () => import(...) 中的路径正确,Webpack/Vite 会将这些组件打包到独立的 chunk 中实现懒加载。
  2. 404 路由处理: 动态添加路由后,一定要确保 router.addRoute({ path: '/:pathMatch(.*)*', redirect: '/404' }) 是最后一个添加的路由。
  3. 按钮权限的存储: 上述指令示例中,按钮权限是从当前路由的 meta 中获取。你需要确保在路由导航守卫或生成动态路由时,将每个路由对应的 btnPermissions 正确地设置到其 meta 中。
  4. 权限更新: 如果系统支持用户动态更改权限(如切换角色),需要在权限变更后调用 router.go(0) 刷新页面或手动重置路由状态。
  5. 安全性: 前端权限控制只是为了用户体验和基础防护,真正的权限校验必须在后端 API 层面严格执行
http://www.dtcms.com/a/347227.html

相关文章:

  • 3.Shell 变量入门:变量定义、赋值、取值($var)及环境变量与局部变量区别详解
  • PYTHON让繁琐的工作自动化-列表
  • 07_模块和包
  • UNet改进(34):ACmix-UNet混合架构的PyTorch
  • 动手学深度学习(pytorch版):第六章节—卷积神经网络(1)从全连接层到卷积
  • 避开MES实施的“坑”:详解需求、开发、上线决胜点
  • 自动化知识工作AI代理的工程与产品实现
  • Node.js 和 Express 面试问题总结
  • Ubuntu通过 systemd 管理 gpt4free,需为其创建 g4f.service 文件,定义服务的启动、停止等操作(未实践)
  • Java基础 8.23
  • 【8位数取中间4位数】2022-10-23
  • LangChain4J-基础(整合Spring、RAG、MCP、向量数据库、提示词、流式输出)
  • QT-常用类
  • 【GPT入门】第57课 详解 LLamaFactory 与 XTuner 实现大模型多卡分布式训练的方案与实践
  • calchash.exe和chckhash.exe计算pe文件hash值的两个实用小工具
  • 【Linux系统】命名管道与共享内存
  • 结构化数据与非结构化数据的区别、特点和应用场景
  • Games 101 第四讲 Transformation Cont(视图变换和投影变换)
  • Java22 stream 新特性 窗口算子:GathererOp 和 GatherSink
  • Flink2.0学习笔记:使用HikariCP 自定义sink实现数据库连接池化
  • 权限管理模块
  • 用 Ansible 优雅部署 Kubernetes 1.33.3(RedHat 10)
  • 第一章:启航篇 —— 新晋工程师的生存与扎根 (1)
  • TensorFlow 深度学习 开发环境搭建
  • 通过Java连接并操作MySQL数据库
  • 多智能体篇:智能体的“语言”——ACL协议与消息队列实现
  • 高斯分布的KL散度计算
  • STM32学习笔记19-FLASH
  • 标准浪涌测试波形对比解析
  • linux内核 - vmalloc 介绍