一個簡單的Vue按鈕級權限方案

演示前端

場景

在年初開發一箇中後臺管理系統,功能涉及到了各個部門(產品、客服、市場等等),在開始的版本中,我和後端配合使用了花褲衩手摸手系列的權限方案,前期很是nice,可是慢慢的隨着功能增多、業務愈來愈複雜,就變得有些吃力了,由於咱們的權限動態性太大了vue

  1. 手摸手系列權限方案是有比較清晰的權限劃分的,而咱們公司部門的崗位職責有時比較模糊。
  2. 後端採用RBAC權限方案,爲了達到第1點要求,將角色劃分的很細,而且角色有時頻繁變更,致使每一次前端都須要手動維護

爲了解決上面2個痛點,我將原方案進行了一丟丟改造。ios

  1. 前端再也不以角色來控制權限,而是以更小粒度的操做(接口)來控制,也就是前端不關心角色
  2. 路由仍是由前端維護(咱們的後端很排斥維護和他們不相干的東西😂),但改成經過操做列表對權限路由進行過濾
  3. 使用單一的方式(方便維護)控制頁面的局部權限,再也不使用自定義指令方式,而是經過函數式組件,緣由是使用自定義指令有多餘的開銷(插入再移除)

後端的配合:git

  1. 提供一個獲取當前用戶操做列表的接口
  2. 操做列表須要增長一個惟一標識(操做碼)供前端使用,不變的
  3. 操做列表須要增長一個routerName字段,用於可視化權限編輯

有一些注意點:github

  1. 好比一個有權限的列表頁面A,同時這個列表接口被權限頁面B使用,如今你配置權限讓某一個用戶沒有A頁面權限,但可使用B頁面,若是你的本意是可使用B頁面的全部功能,這時就會有問題,因此儘可能不要將權限接口跨頁面使用,須要分清哪些數據須要經過字典接口獲取仍是經過權限接口獲取
  2. 有些人可能會糾結,前端維護權限安全嗎?確定是不安全的,安全性主要還在後端這邊把控,後端作好數據和接口方面的權限控制,前端作權限控制我認爲主要仍是爲了交互體驗等。沒有權限你爲何要讓我看到那一坨?
  3. 在使用這種方式以前,要明確當前場景是否確實須要這麼作,畢竟在項目比較大且接口不少的狀況下,你跟操做碼之間有一場持久戰

實現

操做列表示例

以Restful風格接口爲例ajax

const operations = [
  {
    url: '/xxx',
    type: 'get',
    name: '查詢xxx',
    routeName: 'route1', // 接口對應的路由
    opcode: 'XXX_GET' // 操做碼,不變的
  },
  {
    url: '/xxx',
    type: 'post',
    name: '新增xxx',
    routeName: 'route1',
    opcode: 'XXX_POST'
  },
  // ......
]
複製代碼

路由的變化

在路由的meta中增長一個配置字段如requireOps,值可能爲String或者Array,這表示當前路由頁面要顯示的必要的操做碼,Array類型是爲了處理一個路由頁面須要知足同時存在多個操做權限時才顯示的狀況。若值不爲這2種則視爲無權限控制,任何用戶都能訪問axios

因爲最終須要根據過濾後的權限路由動態生成菜單,因此還須要在路由選項中增長几個字段處理顯示問題,其中hidden優先級大於visible後端

  1. hidden,值爲true時,路由包括子路由都不會出如今菜單中
  2. visible,值爲false時,路由不顯示,但顯示子路由
const permissionRoutes = [
  {
    // visible: false,
    // hidden: true,
    path: '/xxx',
    name: 'route1',
    meta: {
      title: '路由1',
      requireOps: 'XXX_GET'
    },
    // ...
  }
]
複製代碼

因爲路由在前端維護,因此以上配置只能寫死,若是後端能贊成維護這一份路由表,那就能夠有不少的發揮空間了,體驗也能作的更好。安全

權限路由過濾

先將權限路由規範一下,同時保留一個副本,可能在可視化時須要用到函數

const routeMap = (routes, cb) => routes.map(route => {
  if (route.children && route.children.length > 0) {
    route.children = routeMap(route.children, cb)
  }
  return cb(route)
})
const hasRequireOps = ops => Array.isArray(ops) || typeof ops === 'string'
const normalizeRequireOps = ops => hasRequireOps(ops)
  ? [].concat(...[ops])
  : null
const normalizeRouteMeta = route => {
  const meta = route.meta = {
    ...(route.meta || {})
  }
  meta.requireOps = normalizeRequireOps(meta.requireOps)
  return route
}

permissionRoutes = routeMap(permissionRoutes, normalizeRouteMeta)
const permissionRoutesCopy = JSON.parse(JSON.stringify(permissionRoutes))
複製代碼

獲取到操做列表後,只須要遍歷權限路由,而後查詢requireOps表明的操做有沒有在操做列表中。這裏須要處理一下requireOps未設置的狀況,若是子路由中都是權限路由,須要爲父級路由自動加上requireOps值,否則當全部子路由都沒有權限時,父級路由就被認爲是無權限控制且可訪問的;而若是子路由中只要有一個路由無權限控制,那就不須要處理父路由。因此這裏能夠用遞歸來解決,先處理子路由再處理父路由

const filterPermissionRoutes = (routes, cb) => {
  // 可能父路由沒有設置requireOps 須要根據子路由肯定父路由的requireOps
  routes.forEach(route => {
    if (route.children) {
      route.children = filterPermissionRoutes(route.children, cb)
      
      if (!route.meta.requireOps) {
        const hasNoPermission = route.children.some(child => child.meta.requireOps === null)
        // 若是子路由中存在不須要權限控制的路由,則跳過
        if (!hasNoPermission) {
          route.meta.requireOps = [].concat(...route.children.map(child => child.meta.requireOps))
        }
      }
    }
  })

  return cb(routes)
}
複製代碼

而後根據操做列表對權限路由進行過濾

let operations = null // 從後端獲取後更新它
const hasOp = opcode => operations
  ? operations.some(op => op.opcode === opcode)
  : false

const proutes = filterPermissionRoutes(permissionRoutes, routes => routes.filter(route => {
  const requireOps = route.meta.requireOps

  if (requireOps) {
    return requireOps.some(hasOp)
  }

  return true
}))

// 動態添加路由
router.addRoutes(proutes)
複製代碼

函數式組件控制局部權限

這個組件實現很簡單,根據傳入的操做碼進行權限判斷,若經過則返回插槽內容,不然返回null。另外,爲了統一風格,支持一下root屬性,表示組件的根節點

const AccessControl = {
  functional: true,
  render (h, { data, children }) {
    const attrs = data.attrs || {}

    // 若是是root,直接透傳
    if (attrs.root !== undefined) {
      return h(attrs.root || 'div', data, children)
    }

    if (!attrs.opcode) {
      return h('span', {
        style: {
          color: 'red',
          fontSize: '30px'
        }
      }, '請配置操做碼')
    }

    const opcodes = attrs.opcode.split(',')

    if (opcodes.some(hasOp)) {
      return children
    }

    return null
  }
}
複製代碼

動態生成權限菜單

以ElementUI爲例,因爲動態渲染須要進行遞歸,若是以文件組件的形式會多一層根組件,因此這裏直接用render function簡單寫一個示例,能夠根據本身的需求改造

// 權限菜單組件
export const PermissionMenuTree = {
  name: 'MenuTree',
  props: {
    routes: {
      type: Array,
      required: true
    },
    collapse: Boolean
  },
  render (h) {
    const createMenuTree = (routes, parentPath = '') => routes.map(route => {
      // hidden: 爲true時當前菜單和子菜單都不顯示
      if (route.hidden === true) {
        return null
      }

      // 子路徑處理
      const fullPath = route.path.charAt(0) === '/' ? route.path : `${parentPath}/${route.path}`

      // visible: 爲false時不顯示當前菜單,但顯示子菜單
      if (route.visible === false) {
        return createMenuTree(route.children, fullPath)
      }

      const title = route.meta.title
      const props = {
        index: fullPath,
        key: route.path
      }

      if (!route.children || route.children.length === 0) {
        return h(
          'el-menu-item',
          { props },
          [h('span', title)]
        )
      }

      return h(
        'el-submenu',
        { props },
        [
          h('span', { slot: 'title' }, title),
          ...createMenuTree(route.children, fullPath)
        ]
      )
    })

    return h(
      'el-menu',
      {
        props: {
          collapse: this.collapse,
          router: true,
          defaultActive: this.$route.path
        }
      },
      createMenuTree(this.routes)
    )
  }
}
複製代碼

接口的權限控制

咱們通常用axios,這裏只須要在axios封裝的基礎上加幾行代碼就能夠了,axios封裝花樣多多,這裏簡單示例

const ajax = axios.create(/* config */)

export default {
  post (url, data, opcode, config = {}) {
    if (opcode && !hasOp(opcode)) {
      return Promise.reject(new Error('沒有操做權限'))
    }
    return ajax.post(url, data, { /* config */ ...config }).then(({ data }) => data)
  },
  // ...
}
複製代碼

到這裏,這個方案差很少就完成了,權限配置的可視化能夠根據操做列表中的routeName來作,將操做與權限路由一一對應,在demo中有一個簡單實現

參考

手摸手,帶你用vue擼後臺 系列二(登陸權限篇)

相關文章
相關標籤/搜索