利用 Vue.js 實現先後端分離的RBAC角色權限管理

項目背景:物業管理後臺,不一樣角色擁有不一樣權限javascript

採用技術:Vue.js + Vuex + Element UI前端

實現 RBAC 權限管理須要後端接口支持,這裏僅提供前端解決方案。
因代碼篇幅較大,對代碼進行了刪減,文中 「...」 即爲省略的一部分代碼。

大體思路:

首先登陸成功後,從後臺拉取用戶當前可顯示的菜單和可用權限列表,分別將其存入 store 的 nav(菜單導航) 和 auth(用戶可用權限) 中,
在用戶切換路由時,判斷是否存在 auth ,若是不存在,則從新獲取,判斷當前訪問地址 to.meta.alias 是否在
用戶可用權限列表中,若是不存在,則提示無權限,不然進入路由。vue

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 採用了異步引入的方式。java

定義路由

// ...
// 系統管理
{
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"
        ]
    }
}

存在的問題

新增 修改 刪除 按鈕還沒法實現根據用戶權限控制其顯示git

代碼上還存在着不足,期待大神可以有更優的解決方案。github

前端代碼

可參考:https://github.com/kfw001/adm...後端

若是有更好的想法和建議,歡迎評論。
相關文章
相關標籤/搜索