浅谈vue权限管理实现及流程


Posted in Javascript onApril 23, 2020

一、整体思路

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

二、实现要点

(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 中存储的各种信息,刷新页面是最保险的。

相关文件的目录截图:

浅谈vue权限管理实现及流程

四、总结

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

优点:
菜单与路由分离,菜单的修改、添加、删除由后端控制,利于后期维护;
使用 addRoutes 动态挂载路由,可控制用户不能在 url 输入相关地址进行跳转;

vue权限管理还有其他实现方式,大家可以根据实际业务考虑做调整,以上的实现方式是比较适合我们现有项目的需求的。以上,有问题欢迎提出交流,喜欢的话点个赞哦~

到此这篇关于浅谈vue权限管理实现及流程的文章就介绍到这了,更多相关vue权限管理内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
基于jQuery的淡入淡出可自动切换的幻灯插件
Aug 24 Javascript
左右悬浮可分组的网站QQ在线客服代码(可谓经典)
Dec 21 Javascript
理运用命名空间让js不产生冲突避免全局变量的泛滥
Jun 15 Javascript
浅谈JavaScript对象与继承
Jul 10 Javascript
jquery.masonry瀑布流效果
May 25 jQuery
js获取html页面代码中图片地址的实现代码
Mar 05 Javascript
微信网页授权并获取用户信息的方法
Jul 30 Javascript
Angular6 Filter实现页面搜索的示例代码
Dec 02 Javascript
详解js根据百度地图提供经纬度计算两点距离
May 13 Javascript
layui动态渲染生成左侧3级菜单的方法(根据后台返回数据)
Sep 23 Javascript
jQuery实现带进度条的轮播图
Sep 13 jQuery
解决vue prop传值default属性如何使用,为何不生效的问题
Sep 21 Javascript
js实现简单的贪吃蛇游戏
Apr 23 #Javascript
Vue Cli3 打包配置并自动忽略console.log语句的方法
Apr 23 #Javascript
vue项目打包之开发环境和部署环境的实现
Apr 23 #Javascript
javascript设计模式 ? 模板方法模式原理与用法实例分析
Apr 23 #Javascript
谈谈我在vue-cli3中用预渲染遇到的坑
Apr 22 #Javascript
如何修改Vue打包后文件的接口地址配置的方法
Apr 22 #Javascript
nuxt+axios实现打包后动态修改请求地址的方法
Apr 22 #Javascript
You might like
Ajax+PHP边学边练 之五 图片处理
2009/12/03 PHP
PHP安全配置详细说明
2011/09/26 PHP
浅谈php提交form表单
2015/07/01 PHP
你需要知道的JavsScript可以做什么?
2007/06/29 Javascript
UserData用法总结 lanyu出品
2010/07/01 Javascript
javascript实现倒计时N秒后网页自动跳转代码
2014/12/11 Javascript
Angularjs 基础入门
2014/12/26 Javascript
JavaScript生成随机字符串的方法
2015/03/19 Javascript
jQuery使用fadeout实现元素渐隐效果的方法
2015/03/27 Javascript
详解JavaScript数组和字符串中去除重复值的方法
2016/03/07 Javascript
jQuery的each循环用法简单示例
2016/06/12 Javascript
浅谈如何实现easyui的datebox格式化
2016/06/12 Javascript
深入理解jquery中的each用法
2016/12/14 Javascript
jQuery日期范围选择器附源码下载
2017/05/23 jQuery
如何理解Vue的.sync修饰符的使用
2017/08/17 Javascript
vue-router的使用方法及含参数的配置方法
2018/11/13 Javascript
微信小程序websocket聊天室的实现示例代码
2019/02/12 Javascript
js时间戳转yyyy-MM-dd HH-mm-ss工具类详解
2019/04/30 Javascript
微信小程序云开发之云函数详解
2019/05/16 Javascript
JS使用H5实现图片预览功能
2019/09/30 Javascript
基于JS判断对象是否是数组
2020/01/10 Javascript
详解微信小程序入门从这里出发(登录注册、开发工具、文件及结构介绍)
2020/07/21 Javascript
JavaScript 实现下雪特效的示例代码
2020/09/09 Javascript
[04:37]DOTA2英雄梦之声Vol20发条
2014/06/20 DOTA
python数组复制拷贝的实现方法
2015/06/09 Python
Python排序算法实例代码
2017/08/10 Python
Python向Excel中插入图片的简单实现方法
2018/04/24 Python
通过实例解析python创建进程常用方法
2020/06/19 Python
TensorFlow中如何确定张量的形状实例
2020/06/23 Python
2014年物业管理工作总结
2014/11/21 职场文书
2015年化妆品销售工作总结
2015/05/11 职场文书
2015年测量员工作总结
2015/05/23 职场文书
走进科学观后感
2015/06/18 职场文书
企业安全生产检查制度
2015/08/06 职场文书
Mysql binlog日志文件过大的解决
2021/10/05 MySQL
SQL Server中使用表变量和临时表
2022/05/20 SQL Server