1. 程式人生 > >Vue + ElementUI 手擼後臺管理網站基本框架(二)許可權控制

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

前端許可權控制的本質

在管理系統中,感覺最讓新手們搞不懂就是許可權管理這部份了,而這也是後臺管理系統中最基礎的部分。

一般說來,許可權管理可以分為兩種情況:第一種為頁面級訪問許可權,第二種為資料級操作許可權。第一種情況是非常常見的,即使用者是否能夠看到頁面;第二種情況是使用者能否對資料進行增刪改查等操作。

這兩種情況在實際的前端表現中都是一致的,即使用者在正常頁面中看不見,看不見就不能操作,所以進行了一次視覺上的隔離。

在這裡有的人可能會說:“你這不是自欺欺人嗎?視覺隔離有什麼用啊,有好多方法能夠繞過去的啊,比如改改程式碼或者直接用ajax訪問”。

是的,你說的對,這其實就是自欺欺人,不過這也正是我要說的:

前端的許可權控制實質上就是用於展示,讓操作變得更加友好,真正的安全實際上是由後端控制的

這裡舉個例子簡單說明一下。如果使用者通過其他方式繞過了前端的路由控制,訪問到了該使用者原本不能訪問的頁面,然後呢?頁面中的資料需要從後端獲取,頁面中的操作也需要傳送給後端,如果後端自身對於所有的請求介面有自己的許可權控制,那麼使用者其實只能看到這個頁面中固有的那些資訊。

這裡其實還有一個大家很容易想到的問題:在做介面測試時,如果系統本身有許可權設定,但是介面未做許可權,那麼測試直接使用API測試工具訪問的話…呵呵

所以說前端根本不用糾結於使用者以各種形式繞過頁面的問題,而以上這些問題在後端那裡則根本不是問題。

許可權策略

在理解了前端許可權的本質後,我們說一下前端的許可權策略。依照我目前的瞭解,大致上把許可權策略分為以下兩種:

  1. 前端記錄所有的許可權。使用者登入後,後端返回使用者角色,前端根據角色自行分配頁面
  2. 前端僅記錄頁面,後端記錄許可權。使用者登陸後,後端返回使用者許可權列表,前端根據該列表生成可訪問頁面

第一種的優點是前端完全控制,想怎麼改就怎麼改;缺點是當角色越來越多時,可能會給前端路由編寫上帶來一定的麻煩。

第二種的優點是前端完全基於後端資料,後期幾乎沒有維護成本;缺點是為了降低維護成本,必須擁有選單配置頁面及許可權分配頁面,否則就是噩夢。

接下來,將一點一點帶你實現前端許可權控制。

介面許可權控制

上文中說到,前端許可權控制中,真正能實現安全的是介面,所以先實現介面的許可權控制,而且這裡聽上去很重要,但實際上卻很簡單。

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

我們通過對axios進行簡單的設定,增加請求攔截器,為每個請求的Header資訊中增加Token。以下為虛擬碼:

const service = axios.create()

// http request 攔截器
// 每次請求都為http頭增加Authorization欄位,其內容為token
service.interceptors.request.use(
    config => {
        config.headers.Authorization = `${token}`
        return config
    }
);
export default service

這樣簡單的設定後即可實現在每次介面許可權控制的前端部分。接下來則是分別講述“頁面級訪問控制”和“資料級操作控制”的前端實現方式。如果對資料級操作控制不感興趣的可以直接跳過。

頁面級訪問許可權控制

雖然頁面級訪問控制實質上應該是控制頁面是否顯示,但落在實際中則有兩種不同的情況:

  1. 顯示系統中所有選單,當用戶訪問不在自己許可權範圍內的頁面時提示許可權不足。
  2. 只顯示當前使用者能訪問的選單,如果使用者通過URL進行強制訪問,則會直接404。

至於第一種形式,個人認為在使用者體驗上非常不好,但不排除有某些特殊場景會用到這種方式。而本篇的許可權控制是基於第二種形式。

依據剛才選定的方式,及上文中提到的許可權策略,我們能夠聯想並梳理出一個大致的流程,這也是本篇中實際許可權的流程:

登入 ——> 獲取該使用者許可權列表 ——> 根據許可權列表生成能夠訪問的選單 ——> 點選選單,進入頁面

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

建立路由表

在建立路由表前,我們先總結一下前端路由表構成的兩種模式:

  1. 同時擁有靜態路由和動態路由。
  2. 只擁有靜態路由

在第一種模式中,將系統中不需要許可權的頁面構成靜態路由,需要許可權的頁面構成動態路由。當用戶登入後,根據返回資料匹配動態路由表,將結果通過addRoutes方法新增到靜態路由中。完整的路由中將只包含當前使用者能訪問的頁面,使用者無權訪問的頁面將直接跳轉到404。(這也是我之前一直使用的模式)

第二種模式則直接將所有頁面都配置到靜態路由中。使用者正常登入,系統將返回資料記錄下來作為許可權資料。當頁面跳轉時,判斷跳轉的頁面是否在使用者的許可權列表中,如果在則正常跳轉,如果不在則可以跳轉到任意其他頁面。

在經過不斷的實踐和改進後,個人認為第二種模式相對簡單,只有單一的路由表,方便以後的管理和維護。同時又因無論哪種模式都不能避免所謂的“安全”問題——個別情況下跳過前端路由情況的發生,所以第二種模式無論怎麼看都比第一種要好很多。

需要注意的是,在第二種模式中,因為只有單一的靜態路由,所以一定要使用vue-router的懶載入策略對路由元件進行載入行為優化,防止首次載入時直接載入全部頁面元件的尷尬問題。當然,你可以對那些不需要許可權的固定頁面不使用懶載入策略,這些頁面包括登入頁、註冊頁等。

關於路由懶載入的知識也可以檢視官方文件。:vue-router懶載入

路由表配置

const staticRoute = [
    {
        path: '/',
        redirect: '/login'
    },
    {
        path: '/login',
        component: () => import(/* webpackChunkName: 'index' */ '../page/login')
    },
    // 你的其他路由
    ...
    // 當頁面地址和上面任一地址不匹配,則跳轉到404
    {
        path: '*',
        redirect: '/404'
    }
]

export default staticRoute

Mock許可權列表資料

在編寫完路由表後,我們需要模擬一個完整的許可權列表資料。這個資料在實際中是使用者登入完成,後臺返回的資料。這裡暫定為以下格式:

模擬返回的許可權列表資料

var data = [
    {
        path: '/home',
        name: '首頁'
    },
    {
        name: '系統元件',
        child: [
            {
                name: '介紹',
                path: '/components'
            },
            {
                name: '功能類',
                child: [
                    {
                        path: '/components/permission',
                        name: '詳細鑑權'
                    },
                    {
                        path: '/components/pageTable',
                        name: '表格分頁'
                    }
                ]
            }
        ]
    }
]    

其中的name和path的值實際上應該在單獨的選單配置頁面中填寫,提交給後臺,讓其記錄角色資訊。

這裡單獨說一下我司現在的做法,以更好的說明該格式的實際使用環境:

  1. 建立系統選單。需要填寫選單的名稱,和頁面地址。多語言條件下填寫多種名稱。
  2. 建立許可權組。需要填寫許可權組名稱,併為該許可權組分配頁面級許可權及資料級許可權。
  3. 分配使用者許可權。將使用者和某個許可權組繫結。

以上配置均在前端有對應的頁面進行操作。

編寫導航鉤子

上文中我們說過,需要在頁面跳轉時,將路由表和返回資料進行匹配,如果存在於返回資料中則正常跳轉,如果不存在則跳轉到任意頁面。這裡我們需要實現這個邏輯。以下為簡單實現,只說明原理,不涉及細節。

// router為vue-router路由物件
router.beforeEach((to, from, next) => {
    // ajax獲取許可權列表函式
    // 這裡省略了一些判斷條件,比如判斷是否已經擁有了許可權資料等
    getPermission().then(res => {
        let isPermission = false
        permissionList.forEach((v) => {
            // 判斷跳轉的頁面是否在許可權列表中
            if(v.path == to.fullPath){
                isPermission = true
            }
        })
        // 沒有許可權時跳轉到401頁面
        if(!isPermission){
            next({path: "/error/401", replace: true})
        } else {
            next()
        }
    })
})

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

資料級操作許可權控制

資料操作許可權的獲取方式,我能夠想到的是以下兩種:

  • 使用者登入後,獲取許可權列表時,直接在該資料中體現
  • 每次訪問頁面時請求後臺,獲取該頁面的資料操作許可權列表

不管專案中具體採用哪一種方法,或者使用兩種方法的結合,其實現原理都是一樣的:即將返回的資料操作許可權列表增加到路由表中的對應頁面的自定義欄位中(如permission),然後在進入頁面前,根據該自定義欄位來判斷頁面中的元素是否需要顯示。

這裡的核心點是:如何將資料放置到路由表中,以及如何根據路由表中的欄位在頁面中進行判斷。接下來則重點解決這兩個問題。

我們先簡單的修改一下登入後返回的許可權列表資料,為其增加permission欄位

var data = [
   ...
    {
        name: '系統元件',
        child: [
            {
                name: '介紹',
                path: '/components',
                // 為介紹頁面增加檢視按鈕許可權
                permission: ['view']
            },
            {
                name: '功能類',
                child: [
                    ...
                    {
                        path: '/components/pageTable',
                        name: '表格',
                        // 為表格頁面增加匯出、編輯許可權
                        permission: ['outport', 'edit']
                    }
                ]
            }
        ]
    }
]    

將資料放置到路由表中

為了實現“將資料放置到路由表中”這個功能,我們只需要將目光放回到上文中說的導航鉤子那裡。在那邊,我們有一個getPermission()函式,用來獲取許可權。那麼我們其實只要在這個函式的基礎上進行修改,把返回的資料直接放到對應路由的meta欄位中。

你可以戳這裡檢視路由meta欄位的官方文件:路由元資訊

// getPermission()
// ajax獲取許可權
axios.get("/permission").then((res) => {
    // 對返回資料扁平化,減少深層次迴圈
    let flatArr = flat(res)
    flatArr.forEach(function(v){
        let routeItem = router.match(v.path)
        if(routeItem){
            // 將返回的所有資料都存到路由的meta資訊中
            routeItem.meta = v
        }
    })
})

根據路由表中的欄位在頁面中進行判斷

實現該功能實際上就是實現一個類似於v-if判斷的外掛。當傳入的引數存在於路由的meta欄位時則渲染該節點,不存在時則不渲染。

這裡期望的用法是這樣的:

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

而實現它則需要使用vue的自定義指令。你可以戳這裡檢視vue自定義指令的說明:自定義指令

const hasPermission = {
    install (Vue, options){
        Vue.directive('hasPermission', {
            bind(el, binding, vnode){
                let permissionList = vnode.context.$route.meta.permission
                if(permissionList && permissionList.length && !permissionList.includes(binding.value)){
                    el.parentNode.removeChild(el)
                }
            }
        })
    }
}

export default hasPermission

以上的方法便是對資料級許可權(實質上是操作按鈕)的顯示控制。

路由控制完整流程圖

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

Created with Raphaël 2.1.2開始進入登入頁面登入成功?是否已經請求過許可權選單?根據資料生成系統選單點選選單,進入頁面前匹配許可權選單,是否有該頁面訪問許可權?獲取該頁面詳細許可權,變更頁面許可權元素進入頁面定製化的無權訪問頁面獲取許可權選單列表獲取成功?yesnoyesnoyesnoyesno

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

在下一章中將重點講述使用常規資料結構生成需要的選單,以及unique-opened模式下點選跟節點不能收回多級選單的問題

原始碼

當前原始碼地址:https://github.com/harsima/vue-backend
請注意,該原始碼會不斷更新(因為工作很忙不能保證定期更新)。原始碼涉及到的東西有超出本篇教程的部分,請酌情閱讀。

本系列目錄