vuejs單頁應用的權限管理實踐
html
在廣告機項目中,角色的權限管理是卡了挺久的一個難點。首先咱們肯定的權限控制分爲兩大部分,其中根據粒的大小分的更細:vue
頁面的權限控制node
下面咱們就看一看是如何實現這些個權限控制的。ios
接口權限就是對用戶的校驗。正常來講,在用戶登陸時服務器須要給前臺返回一個Token,而後在之後前臺每次調用接口時都須要帶上這個Token,git
而後服務端獲取到這個Token後進行比對,若是經過則能夠訪問。github
現有的作法是在登陸成功的回調中將後臺返回的Token直接存儲到sessionStorage
,然在請求時將Token取出放入headers中傳給後臺,代碼以下:vue-router
this.$http({ method: 'get', url: 'test/query?id=20', withCredentials: true, headers: { token: sessionStorage.getItem('token'), name: sessionStorage.getItem('name') //應後臺需求傳的用戶名 } }).then(response => { //請求成功後的操做 })
後來在一些文章中發現axios能夠在攔截器中直接將Token塞入config.headers.Authorization
中,做爲全局傳入。下面是代碼部分:vuex
//main.js import axios from 'axios' // 實例化Axios,並進行超時設置 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
在前面已經說到,頁面權限控制又分爲兩種:數據庫
這些權限通常是在固定頁面進行配置,保存後記錄到數據庫中。axios
按鈕權限暫且不提,頁面訪問權限在實現中又能夠分爲兩種方式:
既然展示出來後不能點,那算幾個意思,逗我玩兒呢?所謂眼不見爲淨,綜合考慮後,確定是方案二比較符合良好的用戶體驗。
好,咱們如今梳理一下大體的頁面訪問權限的流程:
在對流程梳理完成後咱們開始進行詳細的編寫。
建立路由表實際上沒有什麼難度,照着vue-router官方文檔給的示例直接寫就好了。可是由於有部分頁面是不須要訪問權限的,
因此須要將登陸、40四、維護等頁面寫到默認的路由中,而將其它的須要權限的頁面寫到一個變量或者一個文件中,這樣可
以有效的減輕後續的維護壓力。
下面將index.js的代碼貼上,異步路由將適量減小,以避免佔過多篇幅。
// router/index.js import Vue from 'vue' import Router from 'vue-router' import App from '@/App' import store from '../store/index' Vue.use(Router); //手動跳轉的頁面白名單 const whiteList = [ '/' ]; //默認不須要權限的頁面 const constantRouterMap = [ { path: '/', name: '登陸', component: (resolve) => require(['@/components/login'], resolve) }, { path: '/index', name: 'nav.Home', component: (resolve) => require(['@/components/index'], resolve) }, { path: '/templateMake', name: '模板製做', component: (resolve) => require(['@/components/Template/templateMake'], resolve) }, { path: '/programMack', name: '節目製做', component: (resolve) => require(['@/components/Template/programMack'], resolve) }, { path: '/release', name: '節目發佈', component: (resolve) => require(['@/components/Program/release'], resolve) } ] //註冊路由 export const router = new Router({ routes: constantRouterMap }); //異步路由(須要權限的頁面) export const asyncRouterMap = [ { path: '/resource', name: 'nav.Resource', meta: { permission: [] }, component: (resolve) => require(['@/components/Resource/resource'], resolve) }, { path: '/template', name: 'nav.Template', meta: { permission: [] }, component: (resolve) => require(['@/components/Template/template'], resolve) }, { path: '/generalSet', name: 'nav.System', meta: { permission: [] }, component: (resolve) => require(['@/components/SystemSet/generalSet'], resolve) }, { path: '', name: 'nav.Log', component: App, children: [ { path: '/userLog', name: 'nav.UserLog', meta: { permission: [] }, component: (resolve) => require(['@/components/Log/userLog'], resolve), }, { path: '/operatingLog', name: 'nav.SystemLog', meta: { permission: [] }, component: (resolve) => require(['@/components/Log/operatingLog'], resolve), }, ] } ] ];
注意事項:這裏有一個須要很是注意的地方就是 404 頁面必定要最後加載,若是放在constantRouterMap一同聲明瞭404,後面的因此頁面都會被攔截到404,詳細的問題見addRoutes when you've got a wildcard route for 404s does not work
在開始時咱們梳理了一個大體的頁面訪問權限流程。下面咱們先實現最核心的部分:
咱們首先獲取用戶權限列表,在這裏咱們將接觸到vuex狀態管理,官方文檔有詳細介紹,這裏就不過多描述了,下面請看代碼:
// store/index.js import Axios from 'axios' import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex); const axios = Axios.create(); const state = { mode: 'login', list: [] }; const getters = {}; const mutations = { setMode: (state, data) => { state.mode = data }, setList: (state, data) => { state.list = data } }; const actions = { // 獲取權限列表 getPermission({commit}) { return new Promise((resolve, reject) => { axios({ url: '/privilege/queryPrivilege?id=' + sessionStorage.getItem('privId'), methods: 'get', headers: { token: sessionStorage.getItem('token'), name: sessionStorage.getItem('name') } }).then((res) => { // 存儲權限列表 commit('setList', res.data.cust.privileges[0].children); resolve(res.data.cust.privileges[0].children) }).catch(() => { reject() }) }) } }; export default new Vuex.Store({ state, mutations, actions, getters })
好了,咱們如今請求後臺拿到了權限數據,並將數據存放到了vuex中,下面咱們須要利用返回數據匹配以前寫的異步路由表,將匹配結果和靜態路由表結合,開成最終的實際路由表。
其中最關鍵的是利用vue-router2.2.0版本新添加的一個addRoutes方法,咱們看看官方文檔如何解釋此方法的:
router.addRoutes(routes) 2.2.0+
動態添加更多的路由規則。參數必須是一個符合 routes 選項要求的數組。
那咱們如今就能夠開始使用addRoutes進行路由匹配了。下面看代碼:
// router/index.js /** * 根據權限匹配路由 * @param {array} permission 權限列表(菜單列表) * @param {array} asyncRouter 異步路由對象 */ function routerMatch(permission, asyncRouter) { return new Promise((resolve) => { const routers = []; // 建立路由 function createRouter(permission) { // 根據路徑匹配到的router對象添加到routers中便可 permission.forEach((item) => { if (item.children && item.children.length) { createRouter(item.children) } let path = item.path; // 循環異步路由,將符合權限列表的路由加入到routers中 asyncRouter.find((s) => { if (s.path === '') { s.children.find((y) => { if (y.path === path) { y.meta.permission = item.permission; routers.push(s); } }) } if (s.path === path) { s.meta.permission = item.permission; routers.push(s); } }) }) } createRouter(permission) resolve([routers]) }) }
而後咱們編寫導航鉤子
// router/index.js router.beforeEach((to, form, next) => { if (sessionStorage.getItem('token')) { if (to.path === '/') { router.replace('/index') } else { console.log(store.state.list.length); if (store.state.list.length === 0) { //若是沒有權限列表,將從新向後臺請求一次 store.dispatch('getPermission').then(res => { //調用權限匹配的方法 routerMatch(res, asyncRouterMap).then(res => { //將匹配出來的權限列表進行addRoutes router.addRoutes(res[0]); next(to.path) }) }).catch(() => { router.replace('/') }) } else { if (to.matched.length) { next() } else { router.replace('/') } } } } else { if (whiteList.indexOf(to.path) >= 0) { next() } else { router.replace('/') } } });
到這裏咱們已經完成了對頁面訪問的權限控制,接下來咱們來說解一下操做按扭的權限部分。
是否還記得前面的路由配置中咱們多出來的一個代碼,下面咱們拿出來看看:
//異步路由(須要權限的頁面) export const asyncRouterMap = [ { path: '/resource', name: 'nav.Resource', meta: { permission: [] }, component: (resolve) => require(['@/components/Resource/resource'], resolve) }, { path: '/template', name: 'nav.Template', meta: { permission: [] }, component: (resolve) => require(['@/components/Template/template'], resolve) }, { path: '/generalSet', name: 'nav.System', meta: { permission: [] }, component: (resolve) => require(['@/components/SystemSet/generalSet'], resolve) }, { path: '', name: 'nav.Log', component: App, children: [ { path: '/userLog', name: 'nav.UserLog', meta: { permission: [] }, component: (resolve) => require(['@/components/Log/userLog'], resolve), }, { path: '/operatingLog', name: 'nav.SystemLog', meta: { permission: [] }, component: (resolve) => require(['@/components/Log/operatingLog'], resolve), }, ] } ] ];
爲每一個路由頁面增長meta字段。在routerMatch函數中將匹配到的詳細權限字段賦值到這裏。這樣在每一個頁面的route對象中就會獲得這個字段。
asyncRouter.find((s) => { if (s.path === '') { s.children.find((y) => { if (y.path === path) { //賦值 y.meta.permission = item.permission; routers.push(s); } }) } if (s.path === path) { s.meta.permission = item.permission; routers.push(s); } })
接下來咱們編寫一個vue自定義指令對頁面中須要進行鑑權的元素進行判斷,好比相似這樣的:
<a @click="upload" v-allow="'3'"></a> /* 3表明一個上傳權限的ID,權限中有3則顯示按鈕 */
咱們直接註冊一個全局指令,利用vnode來訪問vue的方法。代碼以下:
//main.js //按扭權限指令 Vue.directive('allow', { inserted: (el, binding, vnode) => { let permissionList = vnode.context.$route.meta.permission; if (!permissionList.includes(binding.value)) { el.parentNode.removeChild(el) } } })
至此爲止,權限控制流程就已經徹底結束了,在最後咱們再看一下完整的權限控制流程圖吧.
來源:https://segmentfault.com/a/1190000016445363