Vue + ElementUI 手擼後臺管理網站基本框架(二)許可權控制

蔡俊鋒發表於2017-11-01

  • 介面許可權控制
    頁面許可權控制
    編寫路由表
    頁面訪問許可權
    頁面元素操作許可權
    路由控制完整流程圖
    NEXT登入及系統選單載入

後臺管理系統中,許可權控制一般分為兩個部分。一個是介面訪問的許可權控制,一個是頁面的許可權控制。本章將講述這兩種許可權控制如何實現。本章內容較多,請耐心閱讀。

介面許可權控制


相比於頁面許可權控制,介面許可權控制簡單很多,實現也很簡單,所以寫在最前。

介面許可權控制說白了就是對使用者的校驗。正常來說,在使用者登入時伺服器應給前臺返回一個token,以後前臺每次呼叫介面時都需要帶上這個token,服務端獲取到這個token後進行比對,如果通過則可以訪問。

我們通過對axios進行簡單的設定即可達到這種要求

import axios from 'axios'
// 將token記錄到vuex及cookie中,這部分將在後面章節講述
import store from '../store'

// 超時設定
const service = axios.create({
    timeout: 5000
})
// baseURL
// axios.defaults.baseURL = 'https://api.github.com';

// http request 攔截器
// 每次請求都為http頭增加Authorization欄位,其內容為token
service.interceptors.request.use(
    config => {
        if (store.state.user.token) {
            config.headers.Authorization = `token ${store.state.user.token}`;
        }
        return config
    },
    err => {
        return Promise.reject(err)
    }
);
export default service
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

將以上程式碼寫到單獨檔案中,直接將在入口檔案main.js中註冊到Vue原型上即可。這樣在.vue檔案中直接使用this.$axios即可。當然你也可以不這麼做,在每個需要用到axios的檔案中單獨引入也是可以的。

import axios from './util/ajax'
...
Vue.prototype.$axios = axios
  • 1
  • 2
  • 3

頁面許可權控制


在後臺管理系統中,第一感覺最難最繁瑣的可能就是許可權控制了。在這裡我們先簡單梳理一下許可權都有哪些方面。

頁面許可權主要分為兩個部分

  • 頁面從根本上能否被訪問
  • 頁面中的某些元素是否可以操作

頁面能否被訪問一般都是在登入後,直接獲取該使用者能訪問的頁面列表;或者返回該使用者所在的許可權組進行判斷,但實際中許可權組選單也應該是在系統中可配置的,所以這種許可權組追根究底還是和第一種情況一致。

頁面元素是否可以操作一般都是在進入頁面前再進行鑑權一次。這裡一種方式是通過請求後臺獲取詳細許可權,或者是在上次獲取頁面列表的資料中直接帶著該頁面詳細許可權

根據以上分析,我們總結一下大致流程: 
登入成功 ——> 獲取使用者能訪問的頁面列表(許可權列表) ——> 根據返回資料生成選單 ——> 點選選單——> 進入頁面前 ——> 讀取許可權列表中的該頁面詳細許可權,判斷元素操作許可權 ——> 進入頁面

在對流程梳理完成後我們開始進行詳細的編寫。


編寫路由表

在上文中我們提到許可權頁面,但在實際中我們肯定有不需要許可權,可直接訪問的頁面,比如登入頁面、錯誤頁面、維護頁面等等。我們將這些不需要許可權校驗的頁面寫在預設路由中。同時建議把需要許可權校驗的頁面寫在另一個變數或者檔案中以做區分,這樣無論專案怎麼變化你的程式碼改動量都會很少。

預設路由表,不需要許可權

import Vue from 'vue'
import VueRouter from 'vue-router'
...

const routes = [
    {
        path: '/login',
        component: r => require.ensure([], () => r(require('../page/login/login')), 'login')
    },
    {
        path: '/defaultLayout',
        component: r => require.ensure([], () => r(require('../page/layout/layout')), 'layout'),
        children: [{
            path: '/home',
            component: r => require.ensure([], () => r(require('../page/home/home')), 'home'),
        }]
    },
    {
        path: '/error',
        component: r => require.ensure([], () => r(require('../page/error/error')), 'error'),
        children: [
            {
                path: '/error/401',
                component: r => require.ensure([], () => r(require('../page/error/401')), 'error')
            },
            {
                path: '/error/403',
                component: r => require.ensure([], () => r(require('../page/error/403')), 'error')
            },
            {
                path: '/error/404',
                component: r => require.ensure([], () => r(require('../page/error/404')), 'error')
            }
        ]
    }
]
// 註冊路由
const router = new VueRouter({
    // mode: 'history',
    routes: routes
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

非同步路由表,需要許可權

const asyncRouter = [
    {
        path: '/asyncRouter',
        component: r => require.ensure([], () => r(require('../page/layout/layout')), 'layout'),
        children: []
    },
    {
        path: '/table',
        component: r => require.ensure([], () => r(require('../page/table/table')), 'table')
    },
    {
        path: '/form',
        component: r => require.ensure([], () => r(require('../page/form/form')), 'form'),
    }
 ]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

以上為預設路由和非同步路由的編寫示例。其中針對component欄位進行懶載入及分塊處理,提升首屏載入速度的同時,也可以手動控制讓某些頁面合併到一個單獨的js檔案中,而不是每個頁面都是一個js。

完整解釋請參考官方文件:vue-router懶載入

某些細心的同學可能發現了,在非同步路由表中,第一項貌似是無用資料,但其實並非如此,在下文中會涉及到,所以請不用著急

頁面訪問許可權

還記得我們在上文說過的基本流程嗎?我們先實現最核心的部分:獲取許可權列表——>根據返回資料生成選單 原理上其實就是在路由跳轉前獲取許可權列表,然後根據列表資料進行匹配,將匹配到的結果新增到原來的router物件中

假設由伺服器返回的許可權列表資料如下,我們通過mockjs編寫類似的資料

var data = [
    {
        path: '/home',
        name: '首頁'
    },
    {
        name: '系統元件',
        child: [
            {
                name: '介紹',
                path: '/components'
            },
            {
                name: '功能類',
                child: [
                    {
                        path: '/components/permission',
                        name: '詳細鑑權'
                    },
                    {
                        path: '/components/pageTable',
                        name: '表格分頁'
                    }
                ]
            }
        ]
    }
]    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

編寫導航鉤子

// router為vue-router路由物件
router.beforeEach((to, from, next) => {
    // ajax獲取許可權列表函式,這裡省略該函式詳細內容
    getPermission().then(res => {
        // 匹配許可權列表生成完整的路由
        routerMatch(res, asyncRouter).then(res => {
            // 將匹配到的新路由新增到現在的router物件中
            router.addRoutes(res)
            // 跳轉到對應頁面
            next(to.path)
        })
    })
})

/**
 * 根據非同步路由表中的path欄位進行匹配,生成需要新增的路由物件
 * @param {array} permission 許可權列表(選單列表)
 * @param {array} asyncRouter 非同步路由物件
 */
function routerMatch(permission, asyncRouter){
    return new Promise((resolve) => {
        // 這裡需要獲取完整的已經編譯好的router物件,不可為空陣列,也不能用類router的物件。因為當程式執行到這裡時,vue-router已經解析完畢
        const routers = asyncRouter[0]
        // 建立路由
        function createRouter(permission){
            // 根據路徑匹配到的router物件新增到routers中即可
            // 因permission資料格式不一定相同,所以不寫詳細邏輯了
        }
        createRouter(permission)
        resolve([routers])
    })
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

完成了核心功能後,我們需要對這個鉤子進行完善。當前程式碼是頁面每次呼叫時都請求一次許可權列表,而且隨著頁面跳轉變多,router物件也會越來越大,增加的東西也越來越多,而且還有BUG。還有就是我們的登入頁面、維護頁面、錯誤頁面是不需要許可權校驗的。我們需要對核心部分外圍增加條件判斷。這裡直接上程式碼,邏輯都在程式碼中。

router.beforeEach((to, from, next) => {  
    // 判斷使用者是否登入
    if (Cookies.get('token')) {
        // 如果當前處於登入狀態,並且跳轉地址為login,則自動跳回系統首頁
        // 這種情況出現在手動修改位址列地址時
        if (to.path === '/login') {
            router.replace('/home')
        } else {
            // 頁面跳轉前先判斷是否存在許可權列表,如果存在則直接跳轉,如果沒有則請求一次
            if (store.state.permission.list.length === 0) {
                // 獲取許可權列表,如果失敗則跳回登入頁重新登入
                store.dispatch('permission/getPermission').then(res => {
                    // 匹配並生成需要新增的路由物件
                    routerMatch(res, asyncRouter).then(res => {
                        router.addRoutes(res)
                        next(to.path)
                    })
                }).catch(() => {
                    store.dispatch('user/logout').then(() => {
                        router.replace('/login')
                    })
                })
            } else {
                // 如果跳轉頁面存在於路由中則進入,否則跳轉到404
                // 因為可以通過改變url值進行訪問,所以必須有該判斷
                if(to.matched.length){
                    next()
                } else{
                    router.replace('/error/404')
                }
            }
        }
    } else {
        // 如果是免登陸的頁面則直接進入,否則跳轉到登入頁面
        if (whiteList.indexOf(to.path) >= 0) {
            console.log('該頁面無需登入即可訪問')
            next()
        } else {
            console.log('請重新登入')
            router.replace('/login')
        }
    }
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

到這裡我們已經完成了對頁面訪問的許可權控制。接下來講解頁面元素操作許可權。

頁面元素操作許可權

無論你的頁面元素許可權是在請求頁面訪問許可權時的附屬資料還是在進入前再次請求,其實現原理都是一樣的。以第一種情況為例,我們把之前的返回資料修改一下

var data = [
    {
        path: '/home',
        name: '首頁'
    },
    {
        name: '系統元件',
        child: [
            {
                name: '介紹',
                path: '/components',
                // 為介紹頁面增加檢視按鈕許可權
                permission: ['view']
            },
            {
                name: '功能類',
                child: [
                    {
                        path: '/components/permission',
                        name: '詳細鑑權'
                    },
                    {
                        path: '/components/pageTable',
                        name: '表格',
                        // 為表格頁面增加匯出、編輯許可權
                        permission: ['outport', 'edit']
                    }
                ]
            }
        ]
    }
]    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

如果頁面詳細資料類似這種形式,和頁面訪問許可權資料一同返回,那麼只需要在之前的routerMatch中進行判斷,當然你也需要提前修改好預設路由表和非同步路由表。

const asyncRouter = [
    {
        path: '/asyncRouter',
        component: r => require.ensure([], () => r(require('../page/layout/layout')), 'layout'),
        children: []
    },
    {
        path: '/table',
        // 為每個路由頁面增加meta欄位。在routerMatch函式中將匹配到的詳細許可權欄位賦值到這裡。這樣在每個頁面的route物件中就會得到這個欄位。
        meta: {
            permission: []
        },
        component: r => require.ensure([], () => r(require('../page/table/table')), 'table')
    },
    ...
 ]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

接下來我們需要編寫一個vue的外掛,對頁面中需要進行鑑權的元素進行判斷,比如類似這樣的:

// 如果它有outport許可權則顯示
<el-button v-if="hasPermission('outport')">匯出</el-button>
  • 1
  • 2

我們可以直接寫在每個頁面的methods中,當然更好的方法是利用mixin直接混合到每個頁面。利用mixin之前請確保你的方法是唯一的,因為這個方法會混合到任何vue元件中,不僅僅是你自己的,也包括第三方元件。

const hasPermission = {
    install (Vue, options){
        Vue.mixin({
            methods:{
                hasPermission(data){
                    let permissionList = this.$route.meta.permission
                    if(permissionList && permissionList.length && permissionList.includes(data)){
                        return true
                    }
                    return false
                }
            }
        })
    }
}

export default hasPermission
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

至此為止,後臺管理網站的許可權控制流程就已經完全結束了,在最後我們再看一下完整的許可權控制流程圖吧。

路由控制完整流程圖

開始進入登入頁面登入成功?是否已經請求過許可權選單?根據資料生成完整路由點選選單,進入頁面前獲取該頁面詳細許可權,變更頁面許可權元素進入頁面獲取許可權選單列表獲取成功?yesnoyesnoyesno

NEXT——登入及系統選單載入

相關文章