vue權限篇

前言

在一個項目中,一些功能會涉及到重要的數據管理,爲了確保數據的安全,咱們會在項目中加入權限來限制每一個用戶的操做。做爲前端,咱們要作的是配合後端給到的權限數據,作頁面上的各類各樣的限制。html

需求

由於這是一個工做上的業務需求,因此對於我來講主要有兩個地方須要進行權限控制。前端

第一個是側邊菜單欄,須要控制顯示與隱藏。vue

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

流程

  1. 如何獲取用戶權限?vuex

    後端(當前用戶擁有的權限列表)-> 前端(經過後端的接口獲取到,下文中咱們把當前用戶的權限列表叫作 permissionList)後端

  2. 前端如何作限制?api

    經過產品的需求,在項目中進行權限點的配置,而後經過 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 時須要跳轉到沒有權限的頁面或其餘操做。(路由限制)

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

何時獲取權限,存儲在哪 & 路由限制

我這裏是在 router 的 beforeEach 中獲取的,獲取的 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 切換頁面,但當入庫管理沒有權限時,則應該直接重定向到出庫管理界面。

img

因此想實現以上的效果,我須要重寫 router 的 redirect,作到能夠動態判斷(由於在我配置路由時並不知道當前用戶的權限列表)

而後我查看了 vue-router 的文檔,發現了 redirect 能夠是一個方法,這樣就能夠解決重定向問題了。

vue-router 中 redirect 說明(https://router.vuejs.org/zh/guide/essentials/redirect-and-alias.html#%E9%87%8D%E5%AE%9A%E5%90%91) ,根聽說明咱們能夠改寫 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-router 中 redirect 設置爲 Function 來實現

  4. 當用戶直接輸入沒有權限的 url 時須要跳轉到沒有權限的頁面或其餘操做。(路由限制)

    在 meta 中設置權限, router.beforeEach 中判斷權限。

相關文章
相關標籤/搜索