详解利用 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 相关文章推荐
JavaScript高级程序设计 XML、Ajax 学习笔记
Sep 10 Javascript
代码触发js事件(click、change)示例应用
Dec 13 Javascript
JavaScript字符串对象toUpperCase方法入门实例(用于把字母转换为大写)
Oct 17 Javascript
JavaScript在Android的WebView中parseInt函数转换不正确问题解决方法
Apr 25 Javascript
jQuery三级下拉列表导航菜单代码分享
Apr 15 Javascript
jquery中object对象循环遍历的方法
Dec 18 Javascript
js转html实体的方法
Sep 27 Javascript
详解VueJs异步动态加载块
Mar 09 Javascript
ES6 javascript中class静态方法、属性与实例属性用法示例
Oct 30 Javascript
Vue实现web分页组件详解
Nov 28 Javascript
Vue如何实现响应式系统
Jul 11 Javascript
13 个npm 快速开发技巧(推荐)
Jul 04 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 类型转换函数intval
2009/06/20 PHP
PHP动态创建Web站点的方法
2011/08/14 PHP
浏览器打开层自动缓慢展开收缩实例代码
2013/07/04 Javascript
jQuery实现用户注册的表单验证示例
2013/08/28 Javascript
使用javascript为网页增加夜间模式
2014/01/26 Javascript
JS返回iframe中frameBorder属性值的方法
2015/04/01 Javascript
JavaScript变量的作用域全解析
2015/08/14 Javascript
js随机生成字母数字组合的字符串 随机动画数字
2015/09/02 Javascript
小心!AngularJS结合RequireJS做文件合并压缩的那些坑
2016/01/09 Javascript
JavaScript浏览器对象模型BOM(BrowserObjectModel)实例详解
2016/11/29 Javascript
js中作用域的实例解析
2017/03/16 Javascript
vue2.0 中#$emit,$on的使用详解
2017/06/07 Javascript
webpack配置导致字体图标无法显示的解决方法
2018/03/06 Javascript
Vue 使用中的小技巧
2018/04/26 Javascript
js实现点击选项置顶动画效果
2020/08/25 Javascript
vue+Element-ui前端实现分页效果
2020/11/15 Javascript
Python的lambda匿名函数的简单介绍
2013/04/25 Python
零基础写python爬虫之使用urllib2组件抓取网页内容
2014/11/04 Python
PyQt5每天必学之事件与信号
2018/04/20 Python
解决安装tensorflow遇到无法卸载numpy 1.8.0rc1的问题
2018/06/13 Python
Python切片操作去除字符串首尾的空格
2019/04/22 Python
python批量识别图片指定区域文字内容
2019/04/30 Python
django前端页面下拉选择框默认值设置方式
2020/08/09 Python
css3实现超立体3D图片侧翻倾斜效果
2014/04/16 HTML / CSS
HTML5实现可缩放时钟代码
2017/08/28 HTML / CSS
英国Zoro工具:手动工具,电动工具和个人防护用品
2016/11/02 全球购物
若干个Java基础面试题
2015/05/19 面试题
计算机专业毕业生自荐信
2013/12/31 职场文书
简单英文演讲稿
2014/01/01 职场文书
施工质量承诺书范文
2014/05/30 职场文书
幼儿园中班区域活动总结
2014/07/09 职场文书
机关职员工作检讨书
2014/10/23 职场文书
力克胡哲观后感
2015/06/10 职场文书
学长教您写论文:经验总结
2019/07/09 职场文书
pandas取dataframe特定行列的实现方法
2021/05/24 Python
PostgreSQL怎么创建分区表详解
2022/06/25 PostgreSQL