作者:lxcan

转发链接:https://segmentfault.com/a/1190000022431839

一、整体思路

后端返回用户权限,前端根据用户权限处理得到左侧菜单;所有路由在前端定义好,根据后端返回的用户权限筛选出须要挂载的路由,然后利用 addRoutes 动态挂载路由。

jsp改造vue权限问题Vue30权限治理实现流程实践 Bootstrap

二、实现要点

(1)路由定义,分为初始路由和动态路由,一样平常来说初始路由只有 login,其他路由都挂载在 home 路由之下须要动态挂载。

(2)用户登录,登录成功之后得到 token,保存在 sessionStorage,跳转到 home,此时会进入路由拦截根据 token 获取用户权限列表。

(3)全局路由拦截,根据当前用户有没有 token 和 权限列表进行相应的判断和跳转,当没有 token 时跳到 login,当有 token 而没有权限列表时去发要求获取权限等等逻辑。

(4)处理用户权限,在 store.js 定义一个模块 permission.js,专门用于处理用户权限干系的逻辑,用户权限列表、菜单列表都保存在此模块;

(5)用户权限列表、菜单列表的处理,前真个路由要和后端返回的权限有一个唯一标识(一样平常用路由名做标识符),根据此标识筛选出对应的路由。

(6)左侧菜单,要和用户信息、用户管理模块利用的菜单信息同等,统一利用保存在 store 中的变量。

三、详细实现流程1、准备事情,路由定义

/ router/indes.js // 初始路由 /let router = new Router({ mode: 'history', routes: [ { path: '/login', name: 'login', component: () => import('@/views/login.vue'), }, ]});

/ router/indes.js // 准备动态添加的路由 /export const dynamicRoutes = [ { path: '/', name: 'home', component: () => import('@/views/home.vue'), meta: { requiresAuth: true, }, children: [ // 用户信息 { path: '/user-info', name: 'user-info', component: () => import('@/views/user-setting/user-info.vue'), }, // 修正密码 { path: '/user-password', name: 'user-password', component: () => import('@/views/user-setting/user-password.vue'), }, ] }, { path: '/403', component: () => import('@/views/error-page/403'), }, { path: '', component: () => import('@/views/error-page/404'), },];

系统紧张页面的路由,后续会将这些路由经由权限筛选,添加到 home 路由的 children 里面

/ router/router.js /export default [ // 支配管理 { path: '/deploy-manage', name: 'deploy-manage', component: () => import('@/views/sys-admin/deploy-manage/deploy-manage.vue'), meta: { permitName: 'deploy-manage', } }, // ...];2、用户登录

用户进入登录页,输入用户名、密码、验证码,点击登录,发送登录要求,登录成功之后,将 token 保存在 sessionStorage,然后跳转到首页 /home ,进入路由拦截的逻辑。

/ login.vue /// 发送登录要求vm.$http.login(params, data => { sessionStorage.token = data.token; // ... // 跳转到首页 home。
这里会触发全局路由拦截 router.beforeEach vm.$router.push({ name: 'home' });}, err => { console.log(err);});
3、全局路由拦截

首先从打开本地做事 http://localhost:2001 开始,打开后会进入 login 页面,那么判断的依据是什么?首先是 token。
没有登录的用户是拿不到 token 的,而登录后的用户我们会将 token 存到 seesionStorage,因此,根据当前有没有 token 即可知道是否登录。

/ 全局路由拦截 /router.beforeEach((to, from, next) => { // 根据有没有token判断是否登录 if (!sessionStorage.token) { // 1、当用户打开localhost,to.matched === [],匹配的是空路由,此时须要重定向到login // 2、重定向到login之后,to.matched === [name: "login", path: "/login"...] 便是上一步的login页面 // to.matched.some(item => item.meta.requiresAuth) 这句的意思是 进入的路由页须要登录认证,取反便是不用登录,直接通过 if (to.matched.length > 0 && !to.matched.some(item => item.meta.requiresAuth)) { next(); // 跳过,进入下一个导航钩子。
比如:在 /login 路由页刷新页面会走到此逻辑 } else { next({ path: '/login' }); } } else { // 现在有token了 if (!store.state.permission.permissionList) { // 如果没有 permissionList,发要求获取用户权限列表 store.dispatch('permission/FETCH_PERMISSION').then(() => { next({ path: to.path, query: to.query }); }); } else { // 现在有 permissionList 了 if (to.path !== '/login') { if (to.matched.length === 0) { // 如果匹配到的路由形如 https://172.24.1.117/?id=xxx&name=xxx,表明是关联跳转时没有权限,跳转到403 next({ path: '/403' }); } else if (queryChange) { // 跳转之前将路由中查询字符串为空的过滤掉,如 xxx.com?page=&size= 这种 next({ name: to.name, params: to.params, query: to.query }); } else if (sessionStorage.isSysLock === 'true' && to.path !== '/sys-lock') { next({ path: '/sys-lock' }); } else { next(); } } else { // 1.如果用户手动在地址栏输入 /login,重定向到之前的路由页 // next(from.fullPath); // 2.如果用户手动在地址栏输入 /login,打消token并刷新页面,就会去到登录页 store.commit('goToLogin'); } } }});

(1)当用户打开 localhost,此时还没有 token,匹配的是空路由,我们重定向到登录页 next({ path: '/login' });(2)用户在登录页刷新页面,也会进入路由拦截,此时匹配的是 login 路由,而 login 路由是不须要登录验证的(requiresAuth 为空或者 false),以是直接跳过实行 next();(3)用户在登录页输入了用户名和密码,登录成功,保存了 token,跳转到 /home 路由;(4)此时进入路由拦截,已经有 token了,但是还没有用户权限 permissionList,然后发要求获取用户权限列表,得到权限后 next({ path: to.path, query: to.query }); 连续往下走;(5)再次进入路由拦截,此时有 token 和 permissionList 了,就可以根据实际业务进行跳转了。
上面的代码是判断当前是不是 login 路由,如果用户登录夹帐动在地址栏输入 /login,则打消 token 跳转到登录页。
其他的逻辑就跟详细业务干系了,就不细讲了。

4、处理用户权限

处理用户权限,在 store.js 定义一个模块 permission.js,专门用于处理用户权限干系的逻辑,用户权限列表、菜单列表都保存在此模块;来看看 permission.js 紧张做了什么:

/ permission.js // 由于权限这块逻辑很多,以是在vuex添加了一个permission模块来处理权限干系的逻辑和变量 /import httpRequest from '@/assets/js/service/http'; // http要求import handleModule from '@/assets/js/common/handle-module'; // 处理路由、侧边栏的公共函数import router, { dynamicRoutes } from '@/router/index'; // 默认路由配置,动态路由配置import permissionRouter from '@/router/router'; // 须要权限的路由配置// ...export default { // ... actions: { async FETCH_PERMISSION({ commit, state }) { // 初始化路由表,把稳这里必须写,router.beforeEach 路由拦截时,多次实行 FETCH_PERMISSION commit('setPermission', []); // 发要求获取后端返回的用户权限 let data = await getUserByToken(); let userPopedoms = data.userPopedoms || []; // 保存用户的权限模块(去除掉用户管理和登录),用户管理模块可以利用,权限列表 let userPopeList = userPopedoms.filter(v => v.requestMapping !== 'user-manage' && v.requestMapping !== 'login'); commit('setUserPopedoms', userPopeList); // 根据权限筛选出我们设置好的路由并加入到 path='/' 的children,便是home路由的children下 let routes = handleModule.getRouter(userPopedoms, permissionRouter); let homeContainer = dynamicRoutes.find(v => v.path === '/'); // 利用concat的目的是让 分配给用户的权限处于 children 的第0项 homeContainer.children = routes.concat(homeContainer.children); // 设置首页重定向,重定向到用户权限的第0项 homeContainer.redirect = homeContainer.children[0].name; // 根据权限天生左侧导航菜单 let sidebarMenu = handleModule.getSidebarMenu(userPopeList); commit('setMenu', sidebarMenu); // 初始路由 let initialRoutes = router.options.routes; // 动态添加路由。
只有刷新页面才会清空动态添加的路由信息 router.addRoutes(dynamicRoutes); // 完全的路由表 commit('setPermission', [...initialRoutes, ...dynamicRoutes]); } },};

(1)首先,let data = await getUserByToken(); 发要求获取用户权限,得到 data,data.userPopedoms 格式大致如下:

[ { "moduleGroupId": 1001, "moduleGroupName": "支配管理", "requestMapping": "deploy-manage", }, { "moduleGroupId": 1100, "moduleGroupName": "系统管理", "requestMapping": "sys-manage", "moduleList": [ { "moduleId": 1101, "moduleName": "系统日志", "requestMapping": "system-log", "moduleGroupId": 1100, }, { "moduleId": 1102, "moduleName": "系统告警", "requestMapping": "sys-alert", "moduleGroupId": 1100, }, ], }]

(2)然后,根据我们写好的路由数组,进行比拟,过滤得到我们要的路由。
路由格式在上文“路由定义”的 router/router.js 已经提到。
还要根据用户权限处理得到侧边栏菜单。

为此,我们须要两个处理函数,一个根据用户权限列表和路由数组过滤得到终极路由,另一个根据用户权限处理得到侧边栏菜单。
以是其余专门创建了一个文件 handle-module.js 存放这两个函数。

/ handle-module.js /const handleModule = { / 根据后台返回的权限,以及配置好的所有路由,过滤出真实路由 @param {Array} permissionList 后台返回的用户权限列表 @param {Array} allRouter 前端配置好的所有动态路由的凑集 @return {Array} 过滤后的路由 / getRouter(permissionList = [], allRouter = []) { // permissions 的格式为 ["deploy-manage", "system-log"] let permissions = permissionList.reduce((acc, cur) => { if (cur.moduleList && cur.moduleList.length > 0) cur = cur.moduleList; return acc.concat(cur); }, []).map(v => v.requestMapping); return allRouter.filter(item => permissions.includes(item.meta.permitName)); }, / 根据后台返回的权限,天生侧边栏 @param {Array} permissionList 后台返回的用户权限列表 @return {Array} sidebarMenu 天生的侧边栏数组 / getSidebarMenu(permissionList = []) { let sidebarMenu = []; permissionList.forEach(item => { let menuItem = { name: item.requestMapping, title: item.moduleGroupName, }; menuItem.children = (item.moduleList || []).map(child => ({ name: child.requestMapping, title: child.moduleName, })); sidebarMenu.push(menuItem); }); return sidebarMenu; }};export default handleModule;

(3)上面得到过滤后的路由数组后,加入到 path 为 '/' 的 children 下面

{ path: '/', name: 'home', component: () => import('@/views/home.vue'), meta: { requiresAuth: true, }, children: [ / 将上面得到的路由加入到这里 / // 用户信息 { path: '/user-info', name: 'user-info', component: () => import('@/views/user-setting/user-info.vue'), }, ]}

(4)上面根据权限天生侧边栏菜单之后,保存在 store 待用。

(5)上面第三步将动态路由加入到 home 的 children 之后,就可以将 dynamicRoutes 加入到路由中了。
router.addRoutes(dynamicRoutes);

(6)到了这里,路由就添加完了,也便是 FETCH_PERMISSION 操作完毕了,就可以在 action.then 里面调用 next({ path: to.path, query: to.query }); 进去路由,也便是进入 home。
我们上面已经将 home 路由重定向为菜单的第一个路由信息,以是会进入系统菜单的第一个页面。

刷新页面后,根据 router.beforeEach 的判断,有 token 但是没有 permissionList ,会重新触发 action 去发要求获取用户权限,之前的逻辑会重新走一遍,以是没有问题。

退出登录后,须要打消 token 并刷新页面。
由于是通过 addRoutes 添加路由的,而 vue-router 没有删除路由的 api,以是打消路由、打消 store 中存储的各种信息,刷新页面是最保险的。

干系文件的目录截图:

四、总结

缺陷:全局路由守卫里,每次路由跳转都要做判断;每次刷新页面,须要重新发要求获取用户权限;退出登录时,须要刷新一次页面将动态添加的路由以及权限信息清空;

优点:菜单与路由分离,菜单的修正、添加、删除由后端掌握,利于后期掩护;利用 addRoutes 动态挂载路由,可掌握用户不能在 url 输入干系地址进行跳转;

vue权限管理还有其他实现办法,大家可以根据实际业务考虑做调度,以上的实现办法是比较适宜我们现有项目的需求的。
以上,有问题欢迎提出互换,喜好的话点个赞哦~

推举Vue学习资料文章:

《聊聊昨晚尤雨溪现场针对Vue3.0 Beta版本新特性知识点汇总》

《【新】Vue 3.0 Beta 版本发布,你还学的动么?》

《Vue真是太好了 壹万多字的Vue知识点 超详细!》

《Vue + Koa从零打造一个H5页面可视化编辑器——Quark-h5》

《深入浅出Vue3 随着尤雨溪学 TypeScript 之 Ref 【实践】》

《手把手教你深入浅出vue-cli3升级vue-cli4的方法》

《Vue 3.0 Beta 和React 开拓者分别杠上了》

《手把手教你用vue drag chart 实现一个可以拖动 / 缩放的图表组件》

《Vue3 尝鲜》

《总结Vue组件的通信》

《手把手让你成为更好的Vue.js开拓职员的12个技巧和窍门【实践】》

《Vue 开源项目 TOP45》

《2020 年,Vue 受欢迎程度是否会超过 React?》

《尤雨溪:Vue 3.0的设计原则》

《利用vue实现HTML页面天生图片》

《实现全栈收银系统(Node+Vue)(上)》

《实现全栈收银系统(Node+Vue)(下)》

《vue引入原生高德舆图》

《Vue合理配置WebSocket并实现群聊》

《多年vue项目实战履历汇总》

《vue之将echart封装为组件》

《基于 Vue 的两层吸顶踩坑总结》

《Vue插件总结【前端开拓必备】》

《Vue 开拓必须知道的 36 个技巧【近1W字】》

《构建大型 Vue.js 项目的10条建议》

《深入理解vue中的slot与slot-scope》

《手把手教你Vue解析pdf(base64)转图片【实践】》

《利用vue+node搭建前端非常监控系统》

《推举 8 个俊秀的 vue.js 进度条组件》

《基于Vue实现拖拽升级(九宫格拖拽)》

《手摸手,带你用vue撸后台 系列二(登录权限篇)》

《手摸手,带你用vue撸后台 系列三(实战篇)》

《前端框架用vue还是react?清晰比拟两者差异》

《Vue组件间通信几种办法,你用哪种?【实践】》

《浅析 React / Vue 跨端渲染事理与实现》

《10个Vue开拓技巧助力成为更好的工程师》

《手把手教你Vue之父子组件间通信实践讲解【props、$ref 、$emit】》

《1W字长文+多图,带你理解vue的双向数据绑定源码实现》

《深入浅出Vue3 的相应式和以前的差异到底在哪里?【实践】》

《干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React)》

《基于Vue/VueRouter/Vuex/Axios登录路由和接口级拦截事理与实现》

《手把手教你D3.js 实现数据可视化极速上手到Vue运用》

《吃透 Vue 项目开拓实践|16个方面深入前端工程化开拓技巧【上】》

《吃透 Vue 项目开拓实践|16个方面深入前端工程化开拓技巧【中】》

《吃透 Vue 项目开拓实践|16个方面深入前端工程化开拓技巧【下】》

作者:lxcan

转发链接:https://segmentfault.com/a/1190000022431839