如何優雅的在 vue 中新增許可權控制

邪瓶張起靈發表於2019-03-03

前言

在一個專案中,一些功能會涉及到重要的資料管理,為了確保資料的安全,我們會在專案中加入許可權來限制每個使用者的操作。作為前端,我們要做的是配合後端給到的許可權資料,做頁面上的各種各樣的限制。

需求

因為這是一個工作上的業務需求,所以對於我來說主要有兩個地方需要進行許可權控制。

第一個是側邊選單欄,需要控制顯示與隱藏。

第二個就是頁面內的各個按鈕,彈窗等。

流程

  1. 如何獲取使用者許可權?

    後端(當前使用者擁有的許可權列表)-> 前端(通過後端的介面獲取到,下文中我們把當前使用者的許可權列表叫做 permissionList)

  2. 前端如何做限制?

    通過產品的需求,在專案中進行許可權點的配置,然後通過 permissionList 尋找是否有配置的許可權點,有就顯示,沒有就不顯示。

  3. 然後呢?

    沒了。

當我剛開始接到這個需求的時候就是這麼想的,這有什麼難的,不就獲取 permissionList 然後判斷就可以了嘛。後來我才發現真正的需求遠比我想象的複雜。

真正的問題

上面的需求有提到我們主要解決兩個問題,側邊選單欄的顯示 & 頁面內操作。

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

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: '使用者組設定'
          }
        ]
      }
    ]
  },
  {
    path: '/setting',
    name: 'Setting',
    label: '系統設定'
  },
  {
    path: '/login',
    name: 'Login',
    label: '登入'
  }
]

const router = new VueRouter({
  routes
})

export default router
複製程式碼

其中前兩級路由會顯示在側邊欄中,第三級就不會顯示在側邊欄中了。

頁面內操作的許可權設定不需要考慮很多其他東西,我們主要針對側邊欄以及路由進行問題的分析,通過分析,主要有以下幾個問題:

  1. 什麼時候獲取 permissionList,如何儲存 permissionList
  2. 子路由全都沒許可權時不應該顯示本身(例:當使用者列表和使用者組都沒有許可權時,使用者也不應該顯示在側邊欄)
  3. 預設重定向的路由沒有許可權時,應尋找 children 中有許可權的一項重定向(例:使用者路由重定向到使用者列表路由,若使用者列表沒有許可權,則應該重定向到使用者組路由)
  4. 當使用者直接輸入沒有許可權的 url 時需要跳轉到沒有許可權的頁面或其他操作。(路由限制)

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

什麼時候獲取許可權,儲存在哪 & 路由限制

我這裡是在 routerbeforeEach 中獲取的,獲取的 permissionList 是存放在 vuex 中。

原因是考慮到要做路由的限制,以及方便後面專案中對許可權列表的使用,以下是實現的示例:

首先我們加入許可權配置到 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: '使用者組設定',
          meta: {
            permissions: ['U_1_2_2']
          }
        }
      ]
    }
  ]
}
複製程式碼

可以看到我們把許可權加在了 meta 上,是為了更簡單的從 router.beforeEch 中進行許可權判斷,許可權設定為一個陣列,是因為一個頁面可能涉及多個許可權。

接下來我們設定 router.beforeEach :

// 引入專案的 vuex
import store from '@/store'
// 引入判斷是否擁有許可權的函式
import { includePermission } from '@/utils/permission'

router.beforeEach(async (to, from, next) => {
  // 先判斷是否為登入,登入了才能獲取到許可權,怎麼判斷登入就不寫了
  if (!isLogin) {
    try {
      // 這裡獲取 permissionList
      await store.dispatch('getPermissionList')
      // 這裡判斷當前頁面是否有許可權
      const { permissions } = to.meta
      if (permissions) {
        const hasPermission = includePermission(permissions)
        if (!hasPermission) next({ name: 'NoPermission' })
      }
      next()
    }
  } else {
    next({ name: 'Login' })
  }
})
複製程式碼

我們可以看到我們需要一個判斷許可權的方法 & vuex 中的 getPermissionList 如下:

// @/store
export default {
  state: {
    permissionList: []
  },
  mutations: {
    updatePermissionList: (state, payload) => {
      state.permissionList = payload
    }
  },
  actions: {
    getPermissionList: async ({ state, commit }) => {
      // 這裡是為了防止重複獲取
      if (state.permissionList.length) return
      // 傳送請求方法省略
      const list = await api.getPermissionList()
      commit('updatePermissionList', list)
    }
  }
}
複製程式碼
// @/utils/permission
import store from '@/store'

/**
 * 判斷是否擁有許可權
 * @param {Array<string>} permissions - 要判斷的許可權列表
 */
function includePermission (permissions = []) {
  // 這裡要判斷的許可權沒有設定的話,就等於不需要許可權,直接返回 true
  if (!permissions.length) return true
  const permissionList = store.state.permissionList
  return !!permissions.find(permission => permissionList.includes(permission))
}
複製程式碼

重定向問題

以上我們解決了路由的基本配置與許可權如何獲取,怎麼限制路由跳轉,接下來我們要處理的就是重定向問題了。

這一點可能和我們專案本身架構有關,我們專案的側邊欄下還有子級,是以下圖中的 tab 切換展現的,正常情況當點選藥品管理後頁面會重定向到入庫管理的 tab 切換頁面,但當入庫管理沒有許可權時,則應該直接重定向到出庫管理介面。

如何優雅的在 vue 中新增許可權控制

所以想實現以上的效果,我需要重寫 router 的 redirect,做到可以動態判斷(因為在我配置路由時並不知道當前使用者的許可權列表)

然後我檢視了 vue-router 的文件,發現了 redirect 可以是一個方法,這樣就可以解決重定向問題了。

vue-router 中 redirect 說明 ,根據說明我們可以改寫 redirect 如下:

// 我們需要引入判斷許可權方法
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
}
複製程式碼

雖然問題解決了,但是發現這樣寫下去很麻煩,還要修改 router 的配置,所以我們使用一個方法生成:

// @/utils/permission
/**
 * 建立重定向函式
 * @param {Object} redirect - 重定向物件
 * @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 的函式外面篩選,因為許可權是非同步獲取的
    const hasPermissionChildren = permissionChildren.filter(item => includePermission(item.permissions))
    // 預設填寫的重定向的 name
    const defaultName = redirect.name || ''
    // 如果預設重定向沒有許可權,則從 children 中選擇第一個有許可權的路由做重定向
    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
  }
}
複製程式碼

然後我們就可以改寫為:

// 我們需要引入判斷許可權方法
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
/**
 * 建立有許可權的路由配置(多級)
 * @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 配置加上這樣一層函式就可以了:

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: '使用者組設定',
            meta: {
              permissions: ['U_1_2_2']
            }
          }
        ]
      }
    ]
  }
]

export const routes = routesConfig.map(item => createPermissionRouter(item))

const router = new VueRouter({
  routes
})

export default router
複製程式碼

當然這樣寫還有一個好處,其實你並不需要設定 redirect,這樣會自動重定向到 children 的第一個有許可權的路由

側邊欄顯示問題

我們的專案使用的是根據路由的配置來生成側邊欄的,當然會加一些其他的引數來顯示顯示層級等問題,這裡就不寫具體程式碼了,如何解決側邊欄 children 全都無許可權不顯示的問題呢。

這裡我的思路是,把路由的配置也一同更新到 vuex 中,然後側邊欄配置從 vuex 中的配置來讀取。

由於這個地方涉及修改的東西有點多,而且涉及業務,我就不把程式碼拿出來了,你可以自行實驗。

方便團隊部署許可權點的方法

以上我們解決了大部分許可權的問題,那麼還有很多涉及到業務邏輯的許可權點的部署,所以為了團隊中其他人可以優雅簡單的部署許可權點到各個頁面中,我在專案中提供了以下幾種方式來部署許可權:

  1. 通過指令 v-permission 來直接在 template 上設定
<div v-permission="['U_1']"></div>
複製程式碼
  1. 通過全域性方法 this.$permission 判斷,因為有些許可權並非在模版中的
{
  hasPermission () {
    // 通過方法 $permission 判斷是否擁有許可權
    return this.$permission(['U_1_1', 'U_1_2'])
  }
}
複製程式碼

這裡要注意,為了 $permission 方法的返回值是可被監測的,判斷時需要從 this.$store 中來判斷,以下為實現程式碼:

// @/utils/permission
/**
 * 判斷是否擁有許可權
 * @param {Array<string|number>} permissions - 要判斷的許可權列表
 * @param {Object} permissionList - 傳入 store 中的許可權列表以實現資料可監測
 */
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)
    }
  }
}
複製程式碼

以下為指令的實現程式碼(為了不與 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
}
複製程式碼

總結

針對之前的問題,有以下的總結:

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

    router.beforeEach 獲取,儲存在 vuex。

  2. 子路由全都沒許可權時不應該顯示本身(例:當使用者列表和使用者設定都沒有許可權時,使用者也不應該顯示在側邊欄)

    通過儲存路由配置到 vuex 中,生成側邊欄設定,獲取許可權後修改 vuex 中的配置控制顯示 & 隱藏。

  3. 預設重定向的路由沒有許可權時,應尋找 children 中有許可權的一項重定向(例:使用者路由重定向到使用者列表路由,若使用者列表沒有許可權,則應該重定向到使用者組路由)

    通過 vue-routerredirect 設定為 Function 來實現

  4. 當使用者直接輸入沒有許可權的 url 時需要跳轉到沒有許可權的頁面或其他操作。(路由限制)

    在 meta 中設定許可權, router.beforeEach 中判斷許可權。

以上就是我對於這次許可權需求的大體解決思路與程式碼實現,可能並不是很完美,但還是希望可以幫助到你 ^_^

相關文章