浅谈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 相关文章推荐
js脚本学习 比较实用的基础
Sep 07 Javascript
input点击后placeholder中的提示消息消失
Jan 15 Javascript
JS调用某段SQL语句的方法
Oct 20 Javascript
js求数组中全部数字可拼接出的最大整数示例代码
Aug 25 Javascript
Vue上传组件vue Simple Uploader的用法示例
Aug 25 Javascript
详解vue.js数据传递以及数据分发slot
Jan 20 Javascript
Node.js实现一个HTTP服务器的方法示例
May 13 Javascript
微信小程序云开发 生成带参小程序码流程
May 18 Javascript
Vue混入mixins滚动触底的方法
Nov 22 Javascript
纯js+css实现仿移动端淘宝网站的弹出详情框功能
Dec 29 Javascript
jquery实现抽奖功能
Oct 22 jQuery
js实现随机点名
Jan 19 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
采用thinkphp自带方法生成静态html文件详解
2014/06/13 PHP
ThinkPHP行为扩展Behavior应用实例详解
2014/07/22 PHP
yii2.0之GridView自定义按钮和链接用法
2014/12/15 PHP
CI框架支持$_GET的两种实现方法
2016/05/18 PHP
基于PHP生成简单的验证码
2016/06/01 PHP
PHP缩略图生成和图片水印制作
2017/01/07 PHP
PHP ADODB实现分页功能简单示例
2018/05/25 PHP
关于laravel 日志写入失败问题汇总
2019/10/17 PHP
AJAX架构之Dojo篇
2007/04/10 Javascript
javascript this用法小结
2008/12/19 Javascript
JavaScript 基于原型的对象(创建、调用)
2009/10/16 Javascript
JavaScript 对象模型 执行模型
2010/10/15 Javascript
javascript使用eval或者new Function进行语法检查
2010/10/16 Javascript
jQuery表单验证插件formValidator(改进版)
2012/02/03 Javascript
Bootstrap 粘页脚效果
2016/03/28 Javascript
浅谈JavaScript前端开发的MVC结构与MVVM结构
2016/06/03 Javascript
js复制内容到剪贴板代码,js复制代码的简单实例
2016/10/27 Javascript
Node.js的特点详解
2017/02/03 Javascript
微信小程序实现保存图片到相册功能
2018/11/30 Javascript
json 带斜杠时如何解析的实现
2019/08/12 Javascript
使用layui 的layedit定义自己的toolbar方法
2019/09/18 Javascript
[05:17]DOTA2睡衣妹卖萌求签名 CJ第二天全明星影像
2013/07/28 DOTA
python操作CouchDB的方法
2014/10/08 Python
在Python中操作时间之tzset()方法的使用教程
2015/05/22 Python
TensorFlow安装及jupyter notebook配置方法
2017/09/08 Python
python无限生成不重复(字母,数字,字符)组合的方法
2018/12/04 Python
浅谈pytorch中torch.max和F.softmax函数的维度解释
2020/06/28 Python
Original Penguin美国官网:布拉德皮特、强尼德普喜爱的服装品牌
2016/10/25 全球购物
就业推荐表自我鉴定
2014/03/21 职场文书
心理咨询承诺书
2014/05/20 职场文书
计算机网络专业求职信
2014/06/05 职场文书
推广普通话标语
2014/06/27 职场文书
保险公司演讲稿
2014/09/02 职场文书
党员群众路线教育实践活动剖析材料
2014/10/10 职场文书
2015年骨干教师工作总结
2015/05/26 职场文书
Linux下使用C语言代码搭建一个简单的HTTP服务器
2022/04/13 Servers