浅谈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 相关文章推荐
javascript replace方法与正则表达式
Feb 19 Javascript
jQuery bxCarousel实现图片滚动切换效果示例代码
May 15 Javascript
让页面上两个div中的滚动条(滑块)同步运动示例
Aug 07 Javascript
为什么Node.js会这么火呢?Node.js流行的原因
Dec 01 Javascript
jQuery使用prepend()方法在元素前添加内容用法实例
Mar 26 Javascript
jQuery实现移动端滑块拖动选择数字效果
Dec 24 Javascript
JavaScript中两个字符串的匹配
Jun 08 Javascript
javascript经典特效分享 手风琴、轮播图、图片滑动
Sep 14 Javascript
微信小程序调用PHP后台接口 解析纯html文本
Jun 13 Javascript
使用Angular material主题定义自己的组件库的配色体系
Sep 04 Javascript
基于JS判断对象是否是数组
Jan 10 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
PHP curl模拟登录带验证码的网站
2015/11/30 PHP
laravel-admin 在列表页添加自定义按钮的例子
2019/09/30 PHP
PHP获取php,mysql,apche的版本信息及更多服务器信息
2021/03/09 PHP
在IE模态窗口中自由查看HTML源码的方法
2007/03/08 Javascript
找到了一篇jQuery与Prototype并存的冲突的解决方法
2007/08/29 Javascript
DWR Ext 加载数据
2009/03/22 Javascript
jQuery 1.4 15个你应该知道的新特性(译)
2010/01/24 Javascript
基于jQuery选择器的整理集合
2013/04/26 Javascript
用javascript删除当前行,添加行(示例代码)
2013/11/25 Javascript
js中的hasOwnProperty和isPrototypeOf方法使用实例
2014/06/06 Javascript
jQuery控制元素显示、隐藏、切换、滑动的方法总结
2015/04/16 Javascript
js日期相关函数dateAdd,dateDiff,dateFormat等介绍
2016/09/24 Javascript
js导出Excel表格超出26位英文字符的解决方法ES6
2017/11/15 Javascript
JavaScript判断浏览器运行环境的详细方法
2019/06/30 Javascript
合并百度影音的离线数据( with python 2.3)
2015/08/04 Python
总结Python编程中函数的使用要点
2016/03/20 Python
查找python项目依赖并生成requirements.txt的方法
2018/07/10 Python
Python3.6使用tesseract-ocr的正确方法
2018/10/17 Python
Python3爬虫之自动查询天气并实现语音播报
2019/02/21 Python
基于python图书馆管理系统设计实例详解
2020/08/05 Python
Python读写csv文件流程及异常解决
2020/10/20 Python
BeautifulSoup中find和find_all的使用详解
2020/12/07 Python
python opencv图像处理(素描、怀旧、光照、流年、滤镜 原理及实现)
2020/12/10 Python
新西兰演唱会和体育门票网站:Ticketmaster新西兰
2017/10/07 全球购物
Farfetch阿联酋:奢侈品牌时尚购物平台
2019/07/26 全球购物
LightInTheBox法国站:中国跨境电商
2020/03/05 全球购物
Lookfantastic阿联酋官网:英国知名美妆护肤购物网站
2020/05/26 全球购物
中学教师实习自我鉴定
2013/09/28 职场文书
花卉与景观设计系大学生求职信
2013/10/01 职场文书
高三毕业生自我鉴定
2013/12/20 职场文书
党支部特色活动方案
2014/08/20 职场文书
学习焦裕禄同志为人民服务思想汇报
2014/09/10 职场文书
手机销售员岗位职责
2015/04/11 职场文书
2016基督教会圣诞节开幕词
2016/03/04 职场文书
postgresql使用filter进行多维度聚合的解决方法
2021/07/16 PostgreSQL
Python内置数据类型中的集合详解
2022/03/18 Python