基於Vue實現後臺系統權限控制

需求分析

基礎需求

項目的基礎需求是:javascript

  • 系統管理員擁有最大權限,管理全部企業;
  • 企業管理員擁有該企業全部權限,可下放權限給子帳戶;
  • 子帳戶擁有限制權限,且可被隨時更改。
  • 根據權限列表展現隱藏相對應菜單欄、按鈕等。

image.png

咱們在登陸後,將獲取到的用戶權限保存到本地緩存中,以便每次方便獲取權限,判斷權限。html

image.png

但因爲「子帳戶擁有限制權限,且可被隨時更改」這條需求,咱們的項目變得稍稍棘手一點:前端

  • 權限變少:當用戶點擊到本來有權限但修改後沒有權限的相關請求,後端會給出相對應的狀態碼供前端判斷,此時前端能夠根據狀態碼作出相對應的反饋。
  • 權限變多:每一個請求都是通的,後端沒法反饋,前端也無從得知權限何時變多了。咱們要求用戶退出從新登陸,獲取最新的權限樹。

進階需求

然而,咱們的用戶特別懶,產品也特別慣着他們,他們要求——「界面刷新,就獲取最新的權限列表」。vue

看來原來的計劃得改了,獲取到的用戶權限保存到vuex的狀態管理器store中,刷新的時候,從新異步獲取一次權限,再判斷權限。java

image.png

路由結構

根據項目需求,路由結構以下:vuex

routes: [
    {
      path: '/login',
      component: Login,
      name: 'login'
    },
    {
      path: '/enterprise',
      component: Enterprise,
      name: 'Enterprise',
      children: getRouter(routerConfig)
    },
    {
      path: '*',
      redirect: {name: 'login'}
    }
  ]
複製代碼

項目是多頁面項目,在同一個域名下訪問,系統管理員登陸後以/admin爲前綴,企業帳戶登陸後以/enterprise爲前綴,企業系統作權限控制,只列出企業系統部分路由大綱。json

流程構思

從輸入地址到頁面展現,權限控制流程構思以下:後端

image.png

邏輯實現

爲了將路由與權限控制分離,我將權限管理單獨抽出來一個類。數組

所以核心邏輯大體能夠分爲:緩存

  • 狀態管理部分:處理權限樹、存儲權限
  • 權限管理類:判斷是否登陸、判斷是否有權限
  • 權限指令:控制頁面的展現

狀態管理部分

從後端獲取的權限樹格式大體長這樣:

{
"data": [
  {
      "title": "A模塊",
      "name": "module_A",
      "children": [
        {
          "title": "A模塊-1",
          "name": "module_A_1",
          "children": [
            {
              "title": "A模塊-1-新增",
              "name": "A_1_add",
              "children": []            
            },
            {
              "title": "A模塊-1-編輯",
              "name": "A_1_edit",
              "children": []            
            }
          ]
        }
      ]
  },
  {
    "title": "B模塊",
    "name": "module_B",
    "children": [
      {
        "title": "B模塊-刪除",
        "name": "B_del",
        "children": []
      }
    ]
  }
]
}
複製代碼

咱們須要將權限樹扁平化爲如下形式,方便判斷。
這個列表存儲爲store中的 permissions,後期的判斷權限都以它爲依據。

{
  "module_A": {
    "allow": true,
    "redirect": 'module_A_1'
  },
  "module_A_1": {
    "allow": true,
    "redirect": ''
  },
  "A_1_add": {
    "allow": true,
    "redirect": ''
  },
  "A_1_edit": {
    "allow": true,
    "redirect": ''
  },
  "module_B": {
    "allow": true,
    "redirect": ''
  },
  "B_del": {
    "allow": true,
    "redirect": ''
  },
  // ..
}
複製代碼

要提一嘴的是,redirect 用於重定向,默認是沒有的,可是對於有子層級的路由須要設置跳轉。好比A模塊下面還有4個子模塊,可是用戶只擁有A模塊下的2個模塊 A-一、A-2,redirect 設置爲2個模塊中的A-1模塊。

權限管理類

在權限管理以前,在router須要作額外的配置:

一個頁面可能被兩種權限獲取,如編輯頁和新增頁,所以咱們用數組包含權限。

{
    path: 'module_A',
    component: /***/,
    name: 'module_A',
    meta: {
      authority: ['module_A']
    },
    children: [
        {
          path: 'A-1',
          component: /***/,
          name: 'module_A_1',
          meta: {
             authority: ['module_A']
          }
    		},
         {
          path: 'A-1/:id',  
          component: /***/,
          name: 'module_A_1-id',
          meta: {
             authority: ['A_1_add', 'A_1_edit'] /** 編輯頁的authority **/
          }
    	}
   ]
}
複製代碼

權限管理類大體框架以下:

class AuthControl {
  constructor () {
    this.permissions = {} // 權限列表
    this.authMenus = []  // 菜單
  }
  
  // 登陸頁
  routerLogin = {
    name: 'login'
  }

  // 計算有權限的第一個頁面
	get routerHome () {
  	// ...
    return {
        name: key
    }
  }

  // 登陸頁路由守衛
  async loginGuard (to, from, next) {
    // ...
  }
	
  // 企業系統路由守衛
  async EnterpriseGuard (to, from, next) {
     // ...
  }

  // 異步獲取權限
  async getPermissions () {
    
  }

  _loginCheck () {
  
  }

  _permissionCheck (to) {
  
  }
}
複製代碼

router.js 調用這個類:

const control = new AuthControl()

router.beforeEach(async (to, from, next) => {
  if (to.matched[0].path === 'login') {
    await control.loginGuard(to, from, next)
  }
  if (to.matched[0].path === '/enterprise') {
    await control.EnterpriseGuard(to, from, next)
  }
  next()
})
複製代碼

登陸頁路由守衛

登陸的守衛的調用是判斷登陸頁後進行的守衛。

基本邏輯就是:已登陸=>進入登陸後的第一個頁面;未登陸=>調用next()進入。

async loginGuard (to, from, next) {
    const loginStatus = this._loginCheck()

    if (loginStatus) {
      await this.getPermissions()
      next(this.routerHome)
      return
    }
    
    next()
  }
複製代碼

企業系統路由守衛

將登陸頁守衛與企業系統守衛分開就是由於,企業系統守衛邏輯較複雜,若是全部狀況都調用同一個守衛,這邊考慮的會比較多,分開會清晰一些。
因此企業路由守衛是判斷要進入的地址是企業內容相關頁面。

邏輯大體分爲3塊:

  • 是否登陸
  • 該頁面是否擁有權限
  • 該頁面是否須要重定向
async EnterpriseGuard (to, from, next) {
    // 是否登陸
    const loginStatus = this._loginCheck()
    if (!loginStatus) {
      next(this.routerLogin)
      return
    }
    
    // 該頁面是否擁有權限
    const permissionStatus = await this._permissionCheck(to)
    if (!permissionStatus) {
      next(this.routerHome)
      return
    }

    // 該頁面是否須要重定向
    const authority = to.meta.authority[0]
    const redirect = this.permissions[authority].redirect
    if (redirect) {
      next({
        name: redirect
      })
      return
    }

    next()
}
複製代碼

權限檢查

對於 _loginCheck 各家方法不太一致,不贅述。

對於 _permissionCheck ,主要根據前文在路由中的配置判斷:

_permissionCheck (to) {
    const {
      meta: {
        authority = []
      }
    } = to

    return this.getPermissions()
      .then(permissions => {
        const allow = authority.some(d => permissions[d])
        return allow
      })
      .catch((err) => {
        console.info(err)
        return false
      })
 }
複製代碼

權限指令

對於按鈕權限,會有兩種可能:

  1. 隱藏不可見 hidden
  2. 禁用不可點 disabled

個人初步想法是,對於無權限的按鈕,設置屬性的值 —— disabledhidden,這樣能夠不去修改DOM。

const permission = {
  bind (el, binding) {
    const { effect = 'disabled', name: authority } = binding.value
    if (!Store.getters['permissions'][authority]) {
      el[effect] = true
    }
  }
}

/** * hidden 另外需設置樣式: [hidden] { visibility: hidden; display: none; } .el-button[hidden] + .el-button { margin-left: 0; } .el-button[hidden] ~ .el-button { margin-left: 14px; } */
複製代碼
<!-- 隱藏效果 -->
<el-button v-permission="{name: 'A_1_add', effect:'hidden'}">
  新建
</el-button>

<!-- 置灰效果 -->
<el-button v-permission="{name: 'A_1_edit', effect:'disabled'}">
  編輯
</el-button>
複製代碼

可是仍是發現樣式會有點影響,好比按鈕外部包裹的div設置了padding值的狀況,當按鈕所有沒有的時候,margin值會撐起一段空白。

無奈,對於隱藏樣式,仍是採用移除節點的方式:

export const permission = {
  bind (el, binding) {
    const { effect = 'disabled', name: authority } = binding.value
    if (!Store.getters['permissions'][authority]) {
      if (effect === 'hidden') {
        el.parentNode.removeChild(el)
      }
      el[effect] = true
    }
  }
}
複製代碼

後記

事實上,咱們的需求並無結束。

因爲系統管理員擁有最大權限,管理全部企業,他能夠在系統管理員平臺代理登陸並跳轉任意多個企業並擁有該企業的全部權限。但因爲權限管理類單獨抽象出來,處理起來也沒有那麼難。

好比_loginCheck 方法,新增對系統管理員登陸的兼容,routerHome的計算,進入未代理過的企業則返回系統管理員的主頁,等等。

相關文章
相關標籤/搜索