详解利用 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和JS用外部变量获取Ajax返回的参数值的方法实例(超简单)
Jun 17 Javascript
Node.js中创建和管理外部进程详解
Aug 16 Javascript
iframe里使用JavaScript控制主页转向的方法
Apr 03 Javascript
JavaScript的RequireJS库入门指南
Jul 01 Javascript
jquery实现点击向下展开菜单项(伸缩导航)效果
Aug 22 Javascript
jquery图片轮播特效代码分享
Apr 20 Javascript
JavaScript数组操作函数汇总
Aug 05 Javascript
jQuery实用密码强度检测
Mar 02 Javascript
详解JS模块导入导出
Dec 20 Javascript
深入理解react-router 路由的实现原理
Sep 26 Javascript
js根据需要计算数组中重复出现某个元素的个数
Jan 18 Javascript
Vue的v-model的几种修饰符.lazy,.number和.trim的用法说明
Aug 05 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
PHP源代码数组统计count分析
2011/08/02 PHP
PHP解析RSS的方法
2015/03/05 PHP
PHP最常用的正则表达式
2017/02/13 PHP
PHP 7.1中AES加解密方法mcrypt_module_open()的替换方案
2017/10/17 PHP
php 多进程编程父进程的阻塞与非阻塞实例分析
2020/02/22 PHP
利用PHP计算有多少小于当前数字的数字方法示例
2020/08/26 PHP
网页javascript精华代码集
2007/01/24 Javascript
HTML复选框和单选框 checkbox和radio事件介绍
2012/12/12 Javascript
JQuery制作的放大效果的popup对话框(未添加任何jquery plugin)分享
2013/04/28 Javascript
js 弹出框只弹一次(二次修改之后的)
2013/11/26 Javascript
jquery实现input输入框实时输入触发事件代码
2014/01/28 Javascript
分享使用AngularJS创建应用的5个框架
2015/12/05 Javascript
详解能在多种前端框架下使用的表格控件
2017/01/11 Javascript
js实现弹窗暗层效果
2017/01/16 Javascript
js 单引号替换成双引号,双引号替换成单引号的实现方法
2017/02/16 Javascript
jQuery实现动态添加、删除按钮及input输入框的方法
2017/04/27 jQuery
python中readline判断文件读取结束的方法
2014/11/08 Python
Python读取txt文件数据的方法(用于接口自动化参数化数据)
2018/06/27 Python
Python 串口通信的实现
2020/09/29 Python
CSS3属性选择符介绍
2008/10/17 HTML / CSS
性能服装:HYLETE
2018/08/14 全球购物
应届大学生的推荐信
2013/11/20 职场文书
信息专业个人的自我评价
2013/12/27 职场文书
司法局火灾防控方案
2014/06/05 职场文书
销售口号大全
2014/06/11 职场文书
本科毕业生应聘自荐信范文
2014/06/26 职场文书
幼儿园安全责任书范本
2014/07/24 职场文书
2014年民政工作总结
2014/11/26 职场文书
2014教师专业技术工作总结
2014/12/03 职场文书
工作失误检讨书
2015/01/26 职场文书
2015年服务员工作总结
2015/04/08 职场文书
花木兰观后感
2015/06/10 职场文书
安全教育观后感
2015/06/17 职场文书
电视新闻稿
2015/07/17 职场文书
2016新教师岗前培训心得体会
2016/01/08 职场文书
SpringBoot使用AOP实现统计全局接口访问次数详解
2022/06/16 Java/Android