1. 程式人生 > >Vue2.0使用者許可權控制解決方案

Vue2.0使用者許可權控制解決方案

Vue-Access-Control是一套基於Vue/Vue-Router/axios 實現的前端使用者許可權控制解決方案,通過對路由、檢視、請求三個層面的控制,使開發者可以實現任意顆粒度的使用者許可權控制。

安裝

版本要求

  • Vue 2.0x
  • Vue-router 3.x

獲取

git:git clone https://github.com/tower1229/Vue-Access-Control.git

npm:npm i vue-access-control

執行

1
2
3
4
5
//開發
npm run dev

//構建
npm build

概述

整體思路

會話開始之初,先初始化一個只有登入路由的Vue例項,在根元件created鉤子裡將路由定向到登入頁,使用者登入成功後前端拿到使用者token,設定axios例項統一為請求headers新增{"Authorization":token}

實現使用者鑑權,然後獲取當前使用者的許可權資料,主要包括路由許可權和資源許可權,之後動態新增路由,生成選單,實現許可權指令和全域性許可權驗證方法,併為axios例項新增請求攔截器,至此完成許可權控制初始化。動態載入路由後,路由元件將隨之載入並渲染,而後展現前端介面。

為解決瀏覽器重新整理路由重置的問題,拿到token後要將其儲存到sessionStorage,根元件的created鉤子負責檢查本地是否已有token,如果有則無需登入直接用該token獲取許可權並初始化,如果token有效且當前路由有權訪問,將載入路由元件並正確展現;若當前路由無權訪問將按路由設定跳轉404;如果token失效,後端應返回4xx狀態碼,前端統一為axios例項新增錯誤攔截器,遇到4xx狀態碼執行退出操作,清除sessionStorage

資料並跳轉到登入頁,讓使用者重新登入。

最小依賴原則

Vue-Access-Control的定位是單一領域解決方案,除了Vue/Vue-Router/axios之外沒有其他依賴,理論上可以無障礙的應用到任何有許可權控制需求的Vue專案中,專案基於webpack 模板開發構建,大多數新專案可以直接基於檢出程式碼繼續開發。需要說明的是,專案額外引入的Element-UICryptoJS僅用於開發演示介面,他們不是必須且與許可權控制毫無關係,專案應用中可以自行取捨。

目錄結構

1
2
3
4
5
6
7
8
9
10
11
12
src/
  |-- api/                  //介面檔案
  |     |-- index.js             //輸出通用axios例項
| |-- account.js //按業務模組組織的介面檔案,所有介面都引用./index提供的axios例項 |-- assets/ |-- components/ |-- router/ | |-- fullpath.js //完整路由資料,用於匹配使用者的路由許可權得到實際路由 | `-- index.js //輸出基礎路由例項 |-- views/ |-- App.vue ·-- main.js

資料格式約定

  • 路由許可權資料必須是如下格式的物件陣列,idparent_id相同的兩個路由具有上下級關係,如果希望使用自定義格式的路由資料,需要修改路由控制的相關實現,詳見路由控制

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    [
        {
          "id": "1",
          "name": "選單1",
          "parent_id": null,
          "route": "route1"
        },
        {
          "id": "2",
          "name": "選單1-1",
          "parent_id": "1",
          "route": "route2"
        }
      ]
    
  • 資源許可權資料必須是如下格式的物件陣列,每個物件代表一個RESTful請求,支援帶引數的url,具體格式說明見請求控制

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
     [
        {
          "id": "2c9180895e172348015e1740805d000d",
          "name": "賬號-獲取",
          "url": "/accounts",
          "method": "GET"
        },
        {
          "id": "2c9180895e172348015e1740c30f000e",
          "name": "賬號-刪除",
          "url": "/account/**",
          "method": "DELETE"
        }
    ]
    

路由控制

路由控制包括動態註冊路由和動態生成選單兩部分。

動態註冊路由

最初例項化的路由僅包括登入和404兩個路徑,我們期待完整的路由是這樣的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
[{
  path: '/login',
  name: 'login',
  component: (resolve) => require(['../views/login.vue'], resolve)
}, {
  path: '/404',
  name: '404',
  component: (resolve) => require(['../views/common/404.vue'], resolve)
}, {
  path: '/',
  name: '首頁',
  component: (resolve) => require(['../views/index.vue'], resolve),
  children: [{
    path: '/route1',
    name: '欄目1',
    meta: {
      icon: 'icon-channel1'
    },
    component: (resolve) => require(['../views/view1.vue'], resolve)
  }, {
    path: '/route2',
    name: '欄目2',
    meta: {
      icon: 'ico-channel2'
    },
    component: (resolve) => require(['../views/view2.vue'], resolve),
    children: [{
      path: 'child2-1',
      name: '子欄目2-1',
      meta: {

      },
      component: (resolve) => require(['../views/route2-1.vue'], resolve)
    }]
  }]
}, {
  path: '*',
  redirect: '/404'
}]

那麼接下來就需要獲取首頁以及其子路由們,思路是事先在本地存一份整個專案的完整路由資料,然後根據使用者許可權對完整路由進行篩選。

篩選的實現思路是先將後端返回的路由資料處理成如下雜湊結構:

1
2
3
4
5
6
7
let hashMenus = {
   "/route1":true,
   "/route1/route1-1":true,
   "/route1/route1-2":true,
   "/route2":true,
   ...
}

然後遍歷本地完整路由,在迴圈中將路徑拼接成上述結構中的key格式,通過hashMenus[route]就可以判斷路由是否匹配,具體實現見App.vue檔案中的getRoutes()方法。

如果後端返回的路由許可權資料與約定不同,就需要自行實現篩選邏輯,只要能得到實際可用的路由資料就可以,最終使用addRoutes()方法將他們動態新增到路由例項中,注意404頁面的模糊匹配一定要放在最後。

動態選單

路由資料可以直接用來生成導航選單,但路由資料是在根元件中得到的,導航選單存在於index.vue元件中,顯然我們需要通過某種方式共享選單資料,方法有很多,一般來說首先想到的是Vuex,但選單資料在整個使用者會話過程中不會發生改變,這並不是Vuex的最佳使用場景,而且為了儘量減少不必要的依賴,這裡用了最簡單直接的方法,把選單資料掛在根元件data.menuData上,在首頁裡用this.$parent.menuData獲取。

另外,導航選單很可能會有新增欄目圖示的需求,這可以通過在路由中新增meta資料實現,例如將圖示class或unicode存到路由meta裡,模板中就可以訪問到meta資料,用來生成圖示標籤。

在多角色系統中可能遇到的一個問題是,不同角色有一個名字相同但功能不同的路由,比如說系統管理員企業管理員都有”賬號管理”這個路由,但他們的操作許可權和目標不同,實際上是兩個完全不同的介面,而Vue不允許多個路由同名,因此路由的name必須做區分,但把區分後的name顯示在前端選單上會很不美觀,為了讓不同角色可以享有同一個選單名稱,我們只要將這兩個路由的meta.name都設定成”賬號管理”,在模板迴圈時優先使用meta.name就可以了。

選單的具體實現可以參考views/index.vue

檢視控制

檢視控制的目標是根據當前使用者許可權決定介面元素顯示與否,典型場景是對各種操作按鈕的顯示控制。實現檢視控制的本質是實現一個許可權驗證方法,輸入請求許可權,輸出是否獲准。然後配合v-ifjsx或自定義指令就能靈活實現各種檢視控制。

全域性驗證方法

驗證方法的的實現本身很簡單,無非是根據後端給出的資源許可權做判斷,重點在於優化方法的輸入輸出,提升易用性,經過實踐總結最終使用的方案是,將許可權跟請求同時維護,驗證方法接收請求物件陣列為引數,返回是否具有許可權的布林值。

請求物件格式:

1
2
3
4
5
6
7
//獲取賬戶列表
const request = {
  p: ['get,/accounts'],
  r: params => {
    return instance.get(`/accounts`, {params})
  }
}

許可權驗證方法$_has()的呼叫格式:

1
v-if="$_has([request])"

許可權驗證方法的具體實現見App.vueVue.prototype.$_has方法。

將許可權驗證方法全域性混入,就可以在專案中很容易的配合v-if實現元素顯示控制,這種方式的優點在於靈活,除了可以校驗許可權外,還可以在判斷表示式中加入執行時狀態做更多樣性的判斷,而且可以充分利用v-if響應資料變化的特點,實現動態檢視控制。

自定義指令

v-if的響應特性是把雙刃劍,因為判斷表示式在執行過程中會頻繁觸發,但實際上在一個使用者會話週期內其許可權並不會發生變化,因此如果只需要校驗許可權的話,用v-if會產生大量不必要的運算,這種情況只需在檢視載入時校驗一次即可,可以通過自定義指令實現:

1
2
3
4
5
6
7
8
//許可權指令
Vue.directive('has', {
  bind: function(el, binding) {
    if (!Vue.prototype.$_has(binding.value)) {
      el.parentNode.removeChild(el);
    }
  }
});

自定義指令內部仍然是呼叫全域性驗證方法,但優點在於只會在元素初始化時執行一次,多數情況下都應該使用自定義指令實現檢視控制。

請求控制

請求控制是利用axios攔截器實現的,目的是將越權請求在前端攔截掉,原理是在請求攔截器中判斷本次請求是否符合使用者許可權,以決定是否攔截。

普通請求的判斷很容易,遍歷後端返回的的資源許可權格式,直接判斷request.methodrequest.url是否吻合就可以了,對於帶引數的url需要使用萬用字元,這裡需要根據專案需求前後端協商一致,約定好萬用字元格式後,攔截器中要先將帶引數的url處理成約定格式,再判斷許可權,方案中已經實現了以下兩種萬用字元格式:

1
2
3
4
5
6
7
8
9
1. 格式:/resources/:id
   示例:/resources/1
   url: /resources/**
   解釋:一個名詞後跟一個引數,引數通常表示名詞的id

2. 格式:/store/:id/member
   示例:/store/1/member
   url:/store/*/member
   解釋:兩個名詞之間夾帶一個引數,引數通常表示第一個名詞的id

對於第一種格式需要注意的是,如果你要發起一個url為"/aaa/bbb"的請求,預設會被處理成"/aaa/**"進行許可權校驗,如果這裡的”bbb”並不是引數而是url的一部分,那麼你需要將url改成"/aaa/bbb/",在最後加一個”/“表示該url不需要轉化格式。

攔截器的具體實現見App.vue中的setInterceptor()方法。

如果你的專案還需要其他的萬用字元格式,只需要在攔截器中實現對應的檢測和轉化方法就可以了。

演示及說明

演示說明:

DEMO專案中演示了動態選單、動態路由、按鈕許可權、請求攔截。

演示專案後端由rap2生成mock資料,登入請求通常應該是POST方式,但因為rap2的程式設計模式無法獲取到非GET的請求引數,因此只能用GET方式登入,實際專案中不建議仿效;

另外登入後獲取許可權的介面本來不需要攜帶額外引數,後端可以根據請求頭攜帶的token資訊實現使用者鑑權,但因為rap2的程式設計模式獲取不到headers資料,因此只能增加一個”Authorization”引數用於生成模擬資料。

測試賬號:

1
2
3
4
1. username: root
   password: 任意
2. username: client
   password: 任意

演示地址:

對文章內容有任何疑問歡迎留言討論,或者掃描下方二維碼加入“前端路上-知識星球”付費提問。