如何優(yōu)雅的在 vue 中添加權(quán)限控制

前言

在一個項目中,一些功能會涉及到重要的數(shù)據(jù)管理舅踪,為了確保數(shù)據(jù)的安全,我們會在項目中加入權(quán)限來限制每個用戶的操作贷腕。作為前端咬展,我們要做的是配合后端給到的權(quán)限數(shù)據(jù),做頁面上的各種各樣的限制涮总。

需求

因為這是一個工作上的業(yè)務需求祷舀,所以對于我來說主要有兩個地方需要進行權(quán)限控制。

第一個是側(cè)邊菜單欄抛丽,需要控制顯示與隱藏饰豺。

第二個就是頁面內(nèi)的各個按鈕,彈窗等冤吨。

流程

  1. 如何獲取用戶權(quán)限漩蟆?

    后端(當前用戶擁有的權(quán)限列表)-> 前端(通過后端的接口獲取到,下文中我們把當前用戶的權(quán)限列表叫做 permissionList)

  2. 前端如何做限制怠李?

    通過產(chǎn)品的需求,在項目中進行權(quán)限點的配置夷蚊,然后通過 permissionList 尋找是否有配置的權(quán)限點翘簇,有就顯示,沒有就不顯示呜笑。

  3. 然后呢?

    沒了凰慈。

當我剛開始接到這個需求的時候就是這么想的驼鹅,這有什么難的,不就獲取 permissionList 然后判斷就可以了嘛输钩。后來我才發(fā)現(xiàn)真正的需求遠比我想象的復雜买乃。

真正的問題

上面的需求有提到我們主要解決兩個問題,側(cè)邊菜單欄的顯示 & 頁面內(nèi)操作剪验。

假設(shè)我們有這樣一個路由的設(shè)置(以下只是一個例子):

import VueRouter from 'vue-router'

/* 注意:以下配置僅為部分配置功戚,并且省去了 component 的配置 */

export const routes = [
  {
    path: '/',
    name: 'Admin',
    label: '首頁'
  }, 
  {
    path: '/user',
    name: 'User',
    label: '用戶',
    redirect: { name: 'UserList' },
    children: [
      {
        path: 'list',
        name: 'UserList',
        label: '用戶列表'
      },
      {
        path: 'group',
        name: 'UserGroup',
        label: '用戶組',
        redirect: { name: 'UserGroupList' },
        children: [
          {
            path: 'list',
            name: 'UserGroupList',
            label: '用戶組列表'
          },
          {
            path: 'config',
            name: 'UserGroupConfig',
            label: '用戶組設(shè)置'
          }
        ]
      }
    ]
  },
  {
    path: '/setting',
    name: 'Setting',
    label: '系統(tǒng)設(shè)置'
  },
  {
    path: '/login',
    name: 'Login',
    label: '登錄'
  }
]
const router = new VueRouter({
  routes
})
export default router

其中前兩級路由會顯示在側(cè)邊欄中,第三級就不會顯示在側(cè)邊欄中了届宠。

頁面內(nèi)操作的權(quán)限設(shè)置不需要考慮很多其他東西壳咕,我們主要針對側(cè)邊欄以及路由進行問題的分析,通過分析,主要有以下幾個問題:

  1. 什么時候獲取 permissionList寸谜,如何存儲 permissionList

  2. 子路由全都沒權(quán)限時不應該顯示本身(例:當用戶列表和用戶組都沒有權(quán)限時,用戶也不應該顯示在側(cè)邊欄)

  3. 默認重定向的路由沒有權(quán)限時他爸,應尋找 children 中有權(quán)限的一項重定向(例:用戶路由重定向到用戶列表路由果善,若用戶列表沒有權(quán)限,則應該重定向到用戶組路由)

  4. 當用戶直接輸入沒有權(quán)限的 url 時需要跳轉(zhuǎn)到?jīng)]有權(quán)限的頁面或其他操作讨跟。(路由限制)

下面我們針對以上問題一個一個解決。

什么時候獲取權(quán)限茶袒,存儲在哪 & 路由限制

我這里是在 routerbeforeEach 中獲取的凉馆,獲取的permissionList是存放在 vuex 中。

原因是考慮到要做路由的限制向叉,以及方便后面項目中對權(quán)限列表的使用嗦董,以下是實現(xiàn)的示例:

首先我們加入權(quán)限配置到 router 上:

// 以下只展示部分配置
{
  path: '/user',
  name: 'User',
  label: '用戶',
  meta: {
    permissions: ['U_1']
  },
  redirect: { name: 'UserList' },
  children: [
    {
      path: 'list',
      name: 'UserList',
      label: '用戶列表',
      meta: {
        permissions: ['U_1_1']
      }
    },
    {
      path: 'group',
      name: 'UserGroup',
      label: '用戶組',
      meta: {
        permissions: ['U_1_2']
      },
      redirect: { name: 'UserGroupList' },
      children: [
        {
          path: 'list',
          name: 'UserGroupList',
          label: '用戶組列表',
          meta: {
            permissions: ['U_1_2_1']
          }
        },
        {
          path: 'config',
          name: 'UserGroupConfig',
          label: '用戶組設(shè)置',
          meta: {
            permissions: ['U_1_2_2']
          }
        }
      ]
    }
  ]
}

可以看到我們把權(quán)限加在了 meta 上,是為了更簡單的從 router.beforeEch 中進行權(quán)限判斷销睁,權(quán)限設(shè)置為一個數(shù)組存崖,是因為一個頁面可能涉及多個權(quán)限。

接下來我們設(shè)置 router.beforeEach

// 引入項目的 vuex
import store from '@/store'
// 引入判斷是否擁有權(quán)限的函數(shù)
import { includePermission } from '@/utils/permission'
router.beforeEach(async (to, from, next) => {
  // 先判斷是否為登錄冗栗,登錄了才能獲取到權(quán)限供搀,怎么判斷登錄就不寫了
  if (!isLogin) {
    try {
      // 這里獲取 permissionList
      await store.dispatch('getPermissionList')
      // 這里判斷當前頁面是否有權(quán)限
      const { permissions } = to.meta
      if (permissions) {
        const hasPermission = includePermission(permissions)
        if (!hasPermission) next({ name: 'NoPermission' })
      }
      next()
    }
  } else {
    next({ name: 'Login' })
  }
})

我們可以看到我們需要一個判斷權(quán)限的方法 & vuex 中的 getPermissionList 如下:

// @/store
    export default {
      state: {
        permissionList: []
      },
      mutations: {
        updatePermissionList: (state, payload) => {
          state.permissionList = payload
        }
      },
      actions: {
        getPermissionList: async ({ state, commit }) => {
          // 這里是為了防止重復獲取
          if (state.permissionList.length) return
          // 發(fā)送請求方法省略
          const list = await api.getPermissionList()
          commit('updatePermissionList', list)
        }
      }
    }
// @/utils/permission
import store from '@/store'
/**
 * 判斷是否擁有權(quán)限
 * @param {Array<string>} permissions - 要判斷的權(quán)限列表
 */
function includePermission (permissions = []) {
  // 這里要判斷的權(quán)限沒有設(shè)置的話葛虐,就等于不需要權(quán)限,直接返回 true
  if (!permissions.length) return true
  const permissionList = store.state.permissionList
  return !!permissions.find(permission => permissionList.includes(permission))
}

重定向問題

以上我們解決了路由的基本配置與權(quán)限如何獲取涕蚤,怎么限制路由跳轉(zhuǎn)的诵,接下來我們要處理的就是重定向問題了。

這一點可能和我們項目本身架構(gòu)有關(guān)烦粒,我們項目的側(cè)邊欄下還有子級代赁,是以下圖中的 tab 切換展現(xiàn)的兽掰,正常情況當點擊藥品管理后頁面會重定向到入庫管理的 tab 切換頁面义黎,但當入庫管理沒有權(quán)限時,則應該直接重定向到出庫管理界面泻云。

所以想實現(xiàn)以上的效果狐蜕,我需要重寫 router 的 redirect,做到可以動態(tài)判斷(因為在我配置路由時并不知道當前用戶的權(quán)限列表)

然后我查看了 vue-router 的文檔婆瓜,發(fā)現(xiàn)了 redirect 可以是一個方法贡羔,這樣就可以解決重定向問題了。

vue-router 中 redirect 說明 猴蹂,根據(jù)說明我們可以改寫 redirect 如下:

// 我們需要引入判斷權(quán)限方法

import { includePermission } from '@/utils/permission'
const children = [
  {
    path: 'list',
    name: 'UserList',
    label: '用戶列表',
    meta: {
      permissions: ['U_1_1']
    }
  },
  {
    path: 'group',
    name: 'UserGroup',
    label: '用戶組',
    meta: {
      permissions: ['U_1_2']
    }
  }
]

const routeDemo = {
  path: '/user',
  name: 'User',
  label: '用戶',
  redirect: (to) => {
    if (includePermission(children[0].meta.permissions)) return { name: children[0].name }
    if (includePermission(children[1].meta.permissions)) return { name: children[1].name }
  },
  children
}

雖然問題解決了楣嘁,但是發(fā)現(xiàn)這樣寫下去很麻煩,還要修改 router 的配置聋溜,所以我們使用一個方法生成:

// @/utils/permission
/**
 * 創(chuàng)建重定向函數(shù)
 * @param {Object} redirect - 重定向?qū)ο? * @param {string} redirect.name - 重定向的組件名稱
 * @param {Array<any>} children - 子列表
 */
function createRedirectFn (redirect = {}, children = []) {
  // 避免緩存太大叭爱,只保留 children 的 name 和 permissions
  const permissionChildren = children.map(({ name = '', meta: { permissions = [] } = {} }) => ({ name, permissions }))
  return function (to) {
    // 這里一定不能在 return 的函數(shù)外面篩選,因為權(quán)限是異步獲取的
    const hasPermissionChildren = permissionChildren.filter(item => includePermission(item.permissions))
    // 默認填寫的重定向的 name
    const defaultName = redirect.name || ''
    // 如果默認重定向沒有權(quán)限涤伐,則從 children 中選擇第一個有權(quán)限的路由做重定向
    const firstPermissionName = (hasPermissionChildren[0] || { name: '' }).name
    // 判斷是否需要修改默認的重定向
    const saveDefaultName = !!hasPermissionChildren.find(item => item.name === defaultName && defaultName)
    if (saveDefaultName) return { name: defaultName }
    else return firstPermissionName ? { name: firstPermissionName } : redirect
  }
}

然后我們就可以改寫為:

// 我們需要引入判斷權(quán)限方法
import { includePermission, createRedirectFn } from '@/utils/permission'

const children = [
  {
    path: 'list',
    name: 'UserList',
    label: '用戶列表',
    meta: {
      permissions: ['U_1_1']
    }
  },
  {
    path: 'group',
    name: 'UserGroup',
    label: '用戶組',
    meta: {
      permissions: ['U_1_2']
    }
  }
]
const routeDemo = {
  path: '/user',
  name: 'User',
  label: '用戶',
  redirect: createRedirectFn({ name: 'UserList' }, children),
  children
}

這樣稍微簡潔一些凝果,但我還是需要一個一個路由去修改睦尽,所以我又寫了一個方法來遞歸 router 配置,并重寫他們的 redirect:

// @/utils/permission
/**
 * 創(chuàng)建有權(quán)限的路由配置(多級)
 * @param {Object} config - 路由配置對象
 * @param {Object} config.redirect - 必須是 children 中的一個山害,并且使用 name
 */
function createPermissionRouter ({ redirect, children = [], ...others }) {
  const needRecursion = !!children.length
  if (needRecursion) {
    return {
      ...others,
      redirect: createRedirectFn(redirect, children),
      children: children.map(item => createPermissionRouter(item))
    }
  } else {
    return {
      ...others,
      redirect
    }
  }
}

這樣我們只需要在最外層的 router 配置加上這樣一層函數(shù)就可以了:

import { createPermissionRouter } from '@/utils/permission'

const routesConfig = [
  {
    path: '/user',
    name: 'User',
    label: '用戶',
    meta: {
      permissions: ['U_1']
    },
    redirect: { name: 'UserList' },
    children: [
      {
        path: 'list',
        name: 'UserList',
        label: '用戶列表',
        meta: {
          permissions: ['U_1_1']
        }
      },
      {
        path: 'group',
        name: 'UserGroup',
        label: '用戶組',
        meta: {
          permissions: ['U_1_2']
        },
        redirect: { name: 'UserGroupList' },
        children: [
          {
            path: 'list',
            name: 'UserGroupList',
            label: '用戶組列表',
            meta: {
              permissions: ['U_1_2_1']
            }
          },
          {
            path: 'config',
            name: 'UserGroupConfig',
            label: '用戶組設(shè)置',
            meta: {
              permissions: ['U_1_2_2']
            }
          }
        ]
      }
    ]
  }
]
export const routes = routesConfig.map(item => createPermissionRouter(item))
const router = new VueRouter({
  routes
})
export default router

當然這樣寫還有一個好處浪慌,其實你并不需要設(shè)置 redirect,這樣會自動重定向到 children 的第一個有權(quán)限的路由

側(cè)邊欄顯示問題

我們的項目使用的是根據(jù)路由的配置來生成側(cè)邊欄的权纤,當然會加一些其他的參數(shù)來顯示顯示層級等問題汹想,這里就不寫具體代碼了,如何解決側(cè)邊欄 children 全都無權(quán)限不顯示的問題呢古掏。

這里我的思路是,把路由的配置也一同更新到 vuex 中丧枪,然后側(cè)邊欄配置從 vuex 中的配置來讀取庞萍。

由于這個地方涉及修改的東西有點多,而且涉及業(yè)務屎篱,我就不把代碼拿出來了葵蒂,你可以自行實驗。

方便團隊部署權(quán)限點的方法

以上我們解決了大部分權(quán)限的問題践付,那么還有很多涉及到業(yè)務邏輯的權(quán)限點的部署永高,所以為了團隊中其他人可以優(yōu)雅簡單的部署權(quán)限點到各個頁面中,我在項目中提供了以下幾種方式來部署權(quán)限:

  1. 通過指令v-permission來直接在 template 上設(shè)置
<div v-permission="['U_1']"></div>
  1. 通過全局方法 this.$permission 判斷命爬,因為有些權(quán)限并非在模版中的
{
  hasPermission () {
    // 通過方法 $permission 判斷是否擁有權(quán)限
    return this.$permission(['U_1_1', 'U_1_2'])
  }
}

這里要注意饲宛,為了 $permission 方法的返回值是可被監(jiān)測的,判斷時需要從 this.$store 中來判斷,以下為實現(xiàn)代碼:

// @/utils/permission
    /**
     * 判斷是否擁有權(quán)限
     * @param {Array<string|number>} permissions - 要判斷的權(quán)限列表
     * @param {Object} permissionList - 傳入 store 中的權(quán)限列表以實現(xiàn)數(shù)據(jù)可監(jiān)測
     */
    function includePermissionWithStore (permissions = [], permissionList = []) {
      if (!permissions.length) return true
      return !!permissions.find(permission => permissionList.includes(permission))
    }
import { includePermissionWithStore } from '@/utils/permission'
export default {
  install (Vue, options) {
    Vue.prototype.$permission = function (permissions) {
      const permissionList = this.$store.state.permissionList
      return includePermissionWithStore(permissions, permissionList)
    }
  }
}

以下為指令的實現(xiàn)代碼(為了不與 v-if 沖突久锥,這里控制顯示隱藏通過添加/移除 className 的方式):

// @/directive/permission
import { includePermission } from '@/utils/permission'
const permissionHandle = (el, binding) => {
  const permissions = binding.value
  if (!includePermission(permissions)) {
    el.classList.add('hide')
  } else {
    el.classList.remove('hide')
  }
}
export default {
  inserted: permissionHandle,
  update: permissionHandle
}

總結(jié)

針對之前的問題异剥,有以下的總結(jié):

  1. 什么時候獲取 permissionList,如何存儲 permissionList

    router.beforeEach 獲取冤寿,存儲在 vuex疚沐。

  2. 子路由全都沒權(quán)限時不應該顯示本身(例:當用戶列表和用戶設(shè)置都沒有權(quán)限時,用戶也不應該顯示在側(cè)邊欄)

    通過存儲路由配置到 vuex 中亮蛔,生成側(cè)邊欄設(shè)置,獲取權(quán)限后修改 vuex 中的配置控制顯示 & 隱藏辣吃。

  3. 默認重定向的路由沒有權(quán)限時芬探,應尋找 children 中有權(quán)限的一項重定向(例:用戶路由重定向到用戶列表路由,若用戶列表沒有權(quán)限哩簿,則應該重定向到用戶組路由)

    通過vue-routerredirect 設(shè)置為 Function 來實現(xiàn)

  4. 當用戶直接輸入沒有權(quán)限的 url 時需要跳轉(zhuǎn)到?jīng)]有權(quán)限的頁面或其他操作酝静。(路由限制)

    在 meta 中設(shè)置權(quán)限, router.beforeEach 中判斷權(quán)限别智。

以上就是我對于這次權(quán)限需求的大體解決思路與代碼實現(xiàn),可能并不是很完美讳窟,但還是希望可以幫助到你 _

作者:邪瓶張起靈
文章:https://juejin.im/post/5c7bae3ff265da2db27950f3

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末丽啡,一起剝皮案震驚了整個濱河市硬猫,隨后出現(xiàn)的幾起案子倚评,更是在濱河造成了極大的恐慌馏予,老刑警劉巖盔性,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異蛹尝,居然都是意外死亡悉尾,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進店門愕难,熙熙樓的掌柜王于貴愁眉苦臉地迎上來惫霸,“玉大人,你說我怎么就攤上這事猜丹」杪” “怎么了?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵脉顿,是天一觀的道長。 經(jīng)常有香客問我弊予,道長开财,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任责鳍,我火速辦了婚禮,結(jié)果婚禮上正塌,老公的妹妹穿的比我還像新娘。我一直安慰自己乓诽,他們只是感情好,可當我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布讼育。 她就那樣靜靜地躺著稠集,像睡著了一般。 火紅的嫁衣襯著肌膚如雪剥纷。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天蹲缠,我揣著相機與錄音吼砂,去河邊找鬼鼎文。 笑死,一個胖子當著我的面吹牛拇惋,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蓉坎,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼胡嘿,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了勿侯?” 一聲冷哼從身側(cè)響起缴罗,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤面氓,失蹤者是張志新(化名)和其女友劉穎蛆橡,沒想到半個月后掘譬,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡粥血,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年酿箭,在試婚紗的時候發(fā)現(xiàn)自己被綠了趾娃。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡妇蛀,死狀恐怖笤成,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情纵诞,我是刑警寧澤培遵,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布籽腕,位于F島的核電站,受9級特大地震影響皇耗,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜万伤,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一箭启、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧傅寡,春花似錦放妈、人聲如沸北救。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽珍策。三九已至,卻和暖如春宅倒,著一層夾襖步出監(jiān)牢的瞬間攘宙,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工拐迁, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蹭劈,地道東北人。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓线召,卻偏偏與公主長得像铺韧,于是被迫代替她去往敵國和親缓淹。 傳聞我的和親對象是個殘疾皇子哈打,可洞房花燭夜當晚...
    茶點故事閱讀 44,781評論 2 354

推薦閱讀更多精彩內(nèi)容