详解利用 Vue.js 实现前后端分离的RBAC角色权限管理


Posted in Javascript onSeptember 15, 2017

项目背景:物业管理后台,不同角色拥有不同权限

采用技术:Vue.js + Vuex + Element UI

实现 RBAC 权限管理需要后端接口支持,这里仅提供前端解决方案。

因代码篇幅较大,对代码进行了删减,文中 “...” 即为省略的一部分代码。

大致思路:
首先登录成功后,从后台拉取用户当前可显示的菜单和可用权限列表,分别将其存入 store 的 nav(菜单导航) 和 auth(用户可用权限) 中,在用户切换路由时,判断是否存在 auth ,如果不存在,则重新获取,判断当前访问地址 to.meta.alias 是否在用户可用权限列表中,如果不存在,则提示无权限,否则进入路由。

1. 路由与侧边菜单分离

侧边菜单相关代码 Main.vue

<template>
<!-- ... -->
  <aside :class="collapsed?'menu-collapsed':'menu-expanded'">
    <!--导航菜单-->
    <el-menu :default-active="$route.path"
         class="el-menu-vertical-aliyun" 
         @open="handleopen"
         @close="handleclose"
         @select="handleselect"
         :collapse="collapsed"
         unique-opened router>
      <template v-for="(item,index) in nav">
        <!-- 二级菜单 -->
        <el-submenu :index="index+''"
              v-if="item.children && item.children.length > 0">
          <!-- 二级菜单顶级 -->
          <template slot="title">
            <i :class="['icon',item.iconCls]"></i>
            <span slot="title">{{item.name}}</span>
          </template>
          <!-- 二级菜单下级 -->
          <el-menu-item-group>
            <!--<span slot="title">{{item.name}}</span>-->
            <!-- && child.url-->
            <template v-for="child in item.children">
              <!--无三级菜单-->
              <el-menu-item
                  :index="child.url"
                  :key="child.url"
                  v-if="!child.children">
                {{child.name}}
              </el-menu-item>
              <!--有三级菜单-->
              <el-submenu
                  :index="child.url"
                  :key="child.url"
                  v-if="child.children">
                <span slot="title">{{child.name}}</span>
                <el-menu-item v-for="subChild in child.children"
                       :index="subChild.url"
                       :key="subChild.url">
                  {{subChild.name}}
                </el-menu-item>
              </el-submenu>
            </template>
          </el-menu-item-group>
        </el-submenu>
        <!-- 一级菜单 -->
        <el-menu-item v-if="!item.children"
               :index="item.url">
          <i :class="['icon',item.iconCls]"></i>
          <span slot="title">{{item.name}}</span>
        </el-menu-item>
  
      </template>
    </el-menu>
  </aside>
<!-- ... -->
</template>

<script>
  export default {
    // ...
    computed: {
     // 从 Vuex 中获取导航菜单
     nav() {
      return this.$store.state.nav;
     }
    }
    // ...
  }
</script>

2. 路由切换前进行鉴权

路由定义的部分代码,对每个路由添加了 meta 属性,用于鉴权。

这里 component 采用了异步引入的方式。

定义路由

// ...
// 系统管理
{
path: '/system',
component: Main,
name: '系统管理',
redirect: '/system/organization',
children: [{
 path: '/system/organization',
 component: () => import ('@/views/System/Organization.vue'),
 name: '组织结构',
 // requiresAuth 用于确认此地址是否需要验证
 // alias 用于获取后端返回rbac权限对应的前端路由地址和导航菜单图标
 meta: {requiresAuth: true, alias: 'Pmsadmin/Oragnize/list'}
},
 {
  path: '/system/user',
  component: () => import ('@/views/System/User.vue'),
  name: '人员管理',
  redirect: '/system/user/index',
  children: [
  {
   path: '/system/user/index',
   component: () => import ('@/views/System/UserList.vue'),
   name: '职员列表',
   meta: {requiresAuth: true, alias: 'Pmsadmin/Admin/list'}
  }
  ]
 },
 {
  path: '/system/auth',
  component: () => import ('@/views/System/Auth.vue'),
  name: '角色管理',
  meta: {requiresAuth: true, alias: 'Pmsadmin/Role/list'}
 }
]
}
// ...

路由钩子 beforeEach

router.beforeEach((to, from, next) => {
 document.title = `${configs.title} - ${to.name}`;
 const {hasAuth, auth} = store.state.user;
 // 未拿到权限,则获取
 if (!hasAuth) {
  store.dispatch('getUserAuth');
  console.log('重新获取用户权限');
  // next();
 }
 // 如果未登录,跳转
 if (window.localStorage.getItem('IS_LOGIN') === null && to.path !== '/login') {
  console.log('未登录状态');
  next({
   path: '/login',
   query: {redirect: to.fullPath}
   // 将跳转的路由path作为参数,登录成功后跳转到该路由
  })
 } else {
  // 需要鉴权的路由地址
  console.log(to, auth.indexOf(to.meta.alias), auth);
  if (to.meta.requiresAuth) {
   if (auth.indexOf(to.meta.alias) > -1) {
    console.log('有权限进入');
    next();
   } else {
    if(auth.length > 0) {
     Message.error({
      message: '当前用户权限不足,无法访问',
      showClose: true,
     });
    } else {
     next();
    }
   }
  } else {
   next();
  }
 }
});

在 Vuex 的 state 中,定义好 nav 对象

// 登录用户信息
const user = {
 name: '', // 用户名
 avatar: '', // 用户头像
 auth: [], // 用户权限
 hasAuth: false // 是否已经加载用户权限
};
// 导航菜单
const nav = [];

通过 action 异步获取数据

// 获取用户权限
const getUserAuth = async ({commit}) => {
 const res = await http.post('YOUR_URL', {});
 if (res === null) return;
 console.log('getUserAuth', res.param);
 commit('SET_USER_AUTH', res.param.auth);
 commit('SET_SIDE_NAV', res.param.nav);
};

Vuex 中的 mutation 的相关代码

// 设置用户权限
const SET_USER_AUTH = (state, auth) => {
 state.user.auth = auth.concat('欢迎使用');
 state.user.hasAuth = true;
};
// 设置导航菜单
const SET_SIDE_NAV = (state, nav) => {
 // 导航菜单
 let _nav = [{
  name: '欢迎使用',
  url: "/main",
  iconCls: 'fa fa-bookmark'
 }];
 // 权限菜单对应的路由地址
 const route = {
  "系统管理": {iconCls: 'fa fa-archive', url: ''},
  "Pmsadmin/Oragnize/list": {iconCls: '', url: '/system/organization'},
  "Pmsadmin/Admin/list": {iconCls: '', url: '/system/user/index'},
  "Pmsadmin/Role/list": {iconCls: '', url: '/system/auth'},
  "Pmsadmin/Log/record": {iconCls: '', url: '/system/logs'},
  "项目管理": {iconCls: 'fa fa-unlock-alt', url: ''},
  "Pmsadmin/Project/list": {iconCls: '', url: '/project/list/index'},
  "Pmsadmin/House/list": {iconCls: '', url: '/project/house'},
  "Pmsadmin/Pack/list": {iconCls: '', url: '/project/pack'},
  "广告位": {iconCls: 'fa fa-edit', url: ''},
  "Pmsadmin/Place/list": {iconCls: '', url: '/adsplace/list'},
  "投诉建议": {iconCls: 'fa fa-tasks', url: ''},
  "Pmsadmin/Scategory/list": {iconCls: '', url: '/complain/type'},
  "Pmsadmin/Complain/list": {iconCls: '', url: '/complain/list'},
  "Pmsadmin/Suggest/list": {iconCls: '', url: '/complain/suggestion'},
  "报事报修": {iconCls: 'fa fa-user', url: ''},
  "Pmsadmin/Rcategory/list": {iconCls: '', url: '/rcategory/type'},
  "Pmsadmin/Rcategory/info": {iconCls: '', url: '/rcategory/public'},
  "Pmsadmin/Repair/list": {iconCls: '', url: '/rcategory/personal'},
  "便民服务": {iconCls: 'fa fa-external-link', url: ''},
  "Pmsadmin/Bcategory/list": {iconCls: '', url: '/bcategory/type'},
  "Pmsadmin/Service/list": {iconCls: '', url: '/bcategory/list'},
  "首座推荐": {iconCls: 'fa fa-file-text', url: ''},
  "Pmsadmin/stcategory/list": {iconCls: '', url: '/stcategory/type'},
  "Pmsadmin/Store/list": {iconCls: '', url: '/stcategory/list'},
  "招商租赁": {iconCls: 'fa fa-leaf', url: ''},
  "Pmsadmin/Bussiness/list": {iconCls: '', url: '/bussiness/list'},
  "Pmsadmin/Company/list": {iconCls: '', url: '/bussiness/company'},
  "Pmsadmin/Question/list": {iconCls: '', url: '/bussiness/question'},
  "停车找车": {iconCls: 'fa fa-ra', url: ''},
  "Pmsadmin/Cplace/list": {iconCls: '', url: '/cplace/cmanage'},
  "Pmsadmin/Clist/list": {iconCls: '', url: '/cplace/clist'},
  "Pmsadmin/Cquestion/list": {iconCls: '', url: '/cplace/cquestion'},
 };
 for (let key in nav) {
  let item = nav[key];
  let _temp = {};
  let subItems = []; // 二级菜单临时数组
  if (item.children && item.children.length > 0) {
   // 二级菜单
   item.children.forEach(subItem => {
    subItems.push(Object.assign({}, {
     name: subItem.name || '',
     url: route[subItem.url].url || '',
     iconCls: route[subItem.url].iconCls || '',
    }))
   });
   // 一级菜单
   _temp = Object.assign({}, {
    name: item.name || '',
    url: item.url || '',
    iconCls: route[item.name].iconCls || '',
    children: subItems.slice(0)
   });
   _nav.push(_temp);
  }
 }
 state.nav = _nav;
};

3. 后端接口返回内容

{
  "status": 200,
  "info": "数据查询成功!",
  "param": {
    "nav": {
      "1": {
        "name": "系统管理",
        "url": "",
        "children": [
          {
            "name": "组织结构",
            "url": "Pmsadmin/Oragnize/list"
          },
          {
            "name": "人员管理",
            "url": "Pmsadmin/Admin/list"
          },
          {
            "name": "角色管理",
            "url": "Pmsadmin/Role/list"
          },
          {
            "name": "日志管理",
            "url": "Pmsadmin/Log/record"
          }
        ]
      },
      "61": {
        "name": "广告位",
        "url": "",
        "children": [
          {
            "name": "广告位列表",
            "url": "Pmsadmin/Place/list"
          }
        ]
      }
    },
    "auth": [
      "系统管理",
      "Pmsadmin/Oragnize/list",
      "Pmsadmin/Admin/list",
      "Pmsadmin/Role/list",
      "Pmsadmin/Log/record",
      "广告位",
      "Pmsadmin/Place/list"
    ]
  }
}

存在的问题

  • 新增 修改 删除 按钮还无法实现根据用户权限控制其显示
  • 代码上还存在着不足,期待大神能够有更优的解决方案。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JQuery 动画卷页 返回顶部 动画特效(兼容Chrome)
Feb 15 Javascript
DIV始终居中的js代码
Feb 17 Javascript
jQuery截取指定长度字符串的实现原理及代码
Jul 01 Javascript
jqueryUI里拖拽排序示例分析
Feb 26 Javascript
JS+CSS实现的拖动分页效果实例
May 11 Javascript
Javascript 高阶函数使用介绍
Jun 15 Javascript
在Python中使用glob模块查找文件路径的方法
Jun 17 Javascript
使用jQuery制作遮罩层弹出效果的极简实例分享
May 12 Javascript
关于安卓手机微信浏览器中使用XMLHttpRequest 2上传图片显示字节数为0的解决办法
May 17 Javascript
JavaScript实现单击网页任意位置打开新窗口与关闭窗口的方法
Sep 21 Javascript
详解webpack + vue + node 打造单页面(入门篇)
Sep 23 Javascript
js canvas实现二维码和图片合成的海报
Nov 19 Javascript
动态创建Angular组件实现popup弹窗功能
Sep 15 #Javascript
Vue2.0基于vue-cli+webpack Vuex的用法(实例讲解)
Sep 15 #Javascript
angular4模块中给标签添加背景图的实现方法
Sep 15 #Javascript
基于Vue生产环境部署详解
Sep 15 #Javascript
基于Vue单文件组件详解
Sep 15 #Javascript
json2.js 入门教程之使用方法与实例分析
Sep 14 #Javascript
php main 与 iframe 相互通讯类(js+php同域/跨域)
Sep 14 #Javascript
You might like
如何过滤高亮显示非法字符
2006/10/09 PHP
Joomla下利用configuration.php存储简单数据
2010/05/19 PHP
PHP禁止个别IP访问网站
2013/10/30 PHP
php生成0~1随机小数的方法(必看)
2017/04/05 PHP
PHP7 标准库修改
2021/03/09 PHP
jquery 简单导航实现代码
2009/09/11 Javascript
jQuery插件-jRating评分插件源码分析及使用方法
2012/12/28 Javascript
jQuery ajax全局函数处理session过期后的ajax跳转问题
2016/06/03 Javascript
利用JavaScript判断浏览器类型及版本
2016/08/23 Javascript
详解vue-cli 构建Vue项目遇到的坑
2017/08/30 Javascript
JavaScript面向对象精要(下部)
2017/09/12 Javascript
vue2.0模拟锚点的实例
2018/03/14 Javascript
浅谈TypeScript 用 Webpack/ts-node 运行的配置记录
2019/10/11 Javascript
ant-design-vue 快速避坑指南(推荐)
2020/01/21 Javascript
python 字符串split的用法分享
2013/03/23 Python
Ubuntu下安装PyV8
2016/03/13 Python
浅谈配置OpenCV3 + Python3的简易方法(macOS)
2018/04/02 Python
Python 使用PIL中的resize进行缩放的实例讲解
2018/08/03 Python
基于torch.where和布尔索引的速度比较
2020/01/02 Python
Python连接Oracle之环境配置、实例代码及报错解决方法详解
2020/02/11 Python
pytorch 多分类问题,计算百分比操作
2020/07/09 Python
如何以Winsows Service方式运行JupyterLab
2020/08/30 Python
网页中的电话号码如何实现一键直呼效果_附示例
2016/03/15 HTML / CSS
DAWGS鞋官方网站:鞋,凉鞋,靴子
2016/10/04 全球购物
Android interview questions
2016/12/25 面试题
党员承诺书怎么写
2014/05/20 职场文书
孝敬父母的活动方案
2014/08/31 职场文书
自荐信模板大全
2015/03/27 职场文书
2015年小学图书室工作总结
2015/05/18 职场文书
学术研讨会主持词
2015/07/04 职场文书
工程移交协议书
2016/03/24 职场文书
2019 入党申请书范文
2019/07/10 职场文书
MySQL数据库10秒内插入百万条数据的实现
2021/11/01 MySQL
docker 制作mysql镜像并自动安装
2022/05/20 Servers
win10如何快速切换窗口 win10切换窗口快捷键分享
2022/07/23 数码科技
Java获取字符串编码格式实现思路
2022/09/23 Java/Android