Vue2.0用戶權(quán)限控制解決方案

Vue-Access-Control是一套基于Vue/Vue-Router/axios 實(shí)現(xiàn)的前端用戶權(quán)限控制解決方案,通過對路由翩剪、視圖恋日、請求三個(gè)層面的控制,使開發(fā)者可以實(shí)現(xiàn)任意顆粒度的用戶權(quán)限控制猖凛。

安裝

版本要求

  • Vue 2.0x
  • Vue-router 3.x

獲取

項(xiàng)目主頁://refined-x.com/Vue-Access-Control/

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

npm:npm i vue-access-control

運(yùn)行

//開發(fā)
npm run dev

//構(gòu)建
npm build

概述

整體思路

會話開始之初,先初始化一個(gè)只有登錄路由的Vue實(shí)例绪穆,在根組件created鉤子里將路由定向到登錄頁辨泳,用戶登錄成功后前端拿到用戶token,設(shè)置axios實(shí)例統(tǒng)一為請求headers添加{"Authorization":token}實(shí)現(xiàn)用戶鑒權(quán)玖院,然后獲取當(dāng)前用戶的權(quán)限數(shù)據(jù)菠红,主要包括路由權(quán)限和資源權(quán)限,之后動態(tài)添加路由难菌,生成菜單试溯,實(shí)現(xiàn)權(quán)限指令和全局權(quán)限驗(yàn)證方法,并為axios實(shí)例添加請求攔截器郊酒,至此完成權(quán)限控制初始化遇绞。動態(tài)加載路由后,路由組件將隨之加載并渲染燎窘,而后展現(xiàn)前端界面摹闽。

為解決瀏覽器刷新路由重置的問題,拿到token后要將其保存到sessionStorage褐健,根組件的created鉤子負(fù)責(zé)檢查本地是否已有token付鹿,如果有則無需登錄直接用該token獲取權(quán)限并初始化,如果token有效且當(dāng)前路由有權(quán)訪問蚜迅,將加載路由組件并正確展現(xiàn)舵匾;若當(dāng)前路由無權(quán)訪問將按路由設(shè)置跳轉(zhuǎn)404;如果token失效谁不,后端應(yīng)返回4xx狀態(tài)碼坐梯,前端統(tǒng)一為axios實(shí)例添加錯(cuò)誤攔截器,遇到4xx狀態(tài)碼執(zhí)行退出操作拍谐,清除sessionStorage數(shù)據(jù)并跳轉(zhuǎn)到登錄頁烛缔,讓用戶重新登錄馏段。

最小依賴原則

Vue-Access-Control的定位是單一領(lǐng)域解決方案轩拨,除了Vue/Vue-Router/axios之外沒有其他依賴,理論上可以無障礙的應(yīng)用到任何有權(quán)限控制需求的Vue項(xiàng)目中院喜,項(xiàng)目基于webpack 模板開發(fā)構(gòu)建亡蓉,大多數(shù)新項(xiàng)目可以直接基于檢出代碼繼續(xù)開發(fā)。需要說明的是喷舀,項(xiàng)目額外引入的Element-UICryptoJS僅用于開發(fā)演示界面砍濒,他們不是必須且與權(quán)限控制毫無關(guān)系淋肾,項(xiàng)目應(yīng)用中可以自行取舍。

目錄結(jié)構(gòu)

src/
  |-- api/                  //接口文件
  |     |-- index.js             //輸出通用axios實(shí)例
  |     |-- account.js           //按業(yè)務(wù)模塊組織的接口文件爸邢,所有接口都引用./index提供的axios實(shí)例
  |-- assets/
  |-- components/
  |-- router/
  |     |-- fullpath.js         //完整路由數(shù)據(jù)樊卓,用于匹配用戶的路由權(quán)限得到實(shí)際路由
  |     `-- index.js            //輸出基礎(chǔ)路由實(shí)例
  |-- views/
  |-- App.vue
  ·-- main.js

數(shù)據(jù)格式約定

  • 路由權(quán)限數(shù)據(jù)必須是如下格式的對象數(shù)組,idparent_id相同的兩個(gè)路由具有上下級關(guān)系杠河,如果希望使用自定義格式的路由數(shù)據(jù)碌尔,需要修改路由控制的相關(guān)實(shí)現(xiàn),詳見路由控制
[
    {
      "id": "1",
      "name": "菜單1",
      "parent_id": null,
      "route": "route1"
    },
    {
      "id": "2",
      "name": "菜單1-1",
      "parent_id": "1",
      "route": "route2"
    }
  ]
  • 資源權(quán)限數(shù)據(jù)必須是如下格式的對象數(shù)組券敌,每個(gè)對象代表一個(gè)RESTful請求唾戚,支持帶參數(shù)的url,具體格式說明見請求控制
 [
    {
      "id": "2c9180895e172348015e1740805d000d",
      "name": "賬號-獲取",
      "url": "/accounts",
      "method": "GET"
    },
    {
      "id": "2c9180895e172348015e1740c30f000e",
      "name": "賬號-刪除",
      "url": "/account/**",
      "method": "DELETE"
    }
]

路由控制

路由控制包括動態(tài)注冊路由和動態(tài)生成菜單兩部分待诅。

動態(tài)注冊路由

最初實(shí)例化的路由僅包括登錄和404兩個(gè)路徑叹坦,我們期待完整的路由是這樣的:

[{
  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'
}]

那么接下來就需要獲取首頁以及其子路由們,思路是事先在本地存一份整個(gè)項(xiàng)目的完整路由數(shù)據(jù)卑雁,然后根據(jù)用戶權(quán)限對完整路由進(jìn)行篩選募书。

篩選的實(shí)現(xiàn)思路是先將后端返回的路由數(shù)據(jù)處理成如下哈希結(jié)構(gòu):

let hashMenus = {
   "/route1":true,
   "/route1/route1-1":true,
   "/route1/route1-2":true,
   "/route2":true,
   ...
}

然后遍歷本地完整路由,在循環(huán)中將路徑拼接成上述結(jié)構(gòu)中的key格式测蹲,通過hashMenus[route]就可以判斷路由是否匹配锐膜,具體實(shí)現(xiàn)見App.vue文件中的getRoutes()方法。

如果后端返回的路由權(quán)限數(shù)據(jù)與約定不同弛房,就需要自行實(shí)現(xiàn)篩選邏輯道盏,只要能得到實(shí)際可用的路由數(shù)據(jù)就可以,最終使用addRoutes()方法將他們動態(tài)添加到路由實(shí)例中文捶,注意404頁面的模糊匹配一定要放在最后荷逞。

動態(tài)菜單

路由數(shù)據(jù)可以直接用來生成導(dǎo)航菜單,但路由數(shù)據(jù)是在根組件中得到的粹排,導(dǎo)航菜單存在于index.vue組件中种远,顯然我們需要通過某種方式共享菜單數(shù)據(jù),方法有很多顽耳,一般來說首先想到的是Vuex坠敷,但菜單數(shù)據(jù)在整個(gè)用戶會話過程中不會發(fā)生改變,這并不是Vuex的最佳使用場景射富,而且為了盡量減少不必要的依賴膝迎,這里用了最簡單直接的方法,把菜單數(shù)據(jù)掛在根組件data.menuData上胰耗,在首頁里用this.$parent.menuData獲取限次。

另外,導(dǎo)航菜單很可能會有添加欄目圖標(biāo)的需求,這可以通過在路由中添加meta數(shù)據(jù)實(shí)現(xiàn)卖漫,例如將圖標(biāo)class或unicode存到路由meta里费尽,模板中就可以訪問到meta數(shù)據(jù),用來生成圖標(biāo)標(biāo)簽羊始。

在多角色系統(tǒng)中可能遇到的一個(gè)問題是旱幼,不同角色有一個(gè)名字相同但功能不同的路由,比如說系統(tǒng)管理員和企業(yè)管理員都有”賬號管理”這個(gè)路由突委,但他們的操作權(quán)限和目標(biāo)不同速警,實(shí)際上是兩個(gè)完全不同的界面,而Vue不允許多個(gè)路由同名鸯两,因此路由的name必須做區(qū)分闷旧,但把區(qū)分后的name顯示在前端菜單上會很不美觀,為了讓不同角色可以享有同一個(gè)菜單名稱钧唐,我們只要將這兩個(gè)路由的meta.name都設(shè)置成”賬號管理”忙灼,在模板循環(huán)時(shí)優(yōu)先使用meta.name就可以了。

菜單的具體實(shí)現(xiàn)可以參考views/index.vue钝侠。

視圖控制

視圖控制的目標(biāo)是根據(jù)當(dāng)前用戶權(quán)限決定界面元素顯示與否该园,典型場景是對各種操作按鈕的顯示控制。實(shí)現(xiàn)視圖控制的本質(zhì)是實(shí)現(xiàn)一個(gè)權(quán)限驗(yàn)證方法帅韧,輸入請求權(quán)限里初,輸出是否獲準(zhǔn)。然后配合v-if或jsx或自定義指令就能靈活實(shí)現(xiàn)各種視圖控制忽舟。

全局驗(yàn)證方法

驗(yàn)證方法的的實(shí)現(xiàn)本身很簡單双妨,無非是根據(jù)后端給出的資源權(quán)限做判斷,重點(diǎn)在于優(yōu)化方法的輸入輸出叮阅,提升易用性刁品,經(jīng)過實(shí)踐總結(jié)最終使用的方案是,將權(quán)限跟請求同時(shí)維護(hù)浩姥,驗(yàn)證方法接收請求對象數(shù)組為參數(shù)挑随,返回是否具有權(quán)限的布爾值。

請求對象格式:

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

權(quán)限驗(yàn)證方法$_has()的調(diào)用格式:

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

權(quán)限驗(yàn)證方法的具體實(shí)現(xiàn)見App.vueVue.prototype.$_has方法勒叠。

將權(quán)限驗(yàn)證方法全局混入兜挨,就可以在項(xiàng)目中很容易的配合v-if實(shí)現(xiàn)元素顯示控制,這種方式的優(yōu)點(diǎn)在于靈活眯分,除了可以校驗(yàn)權(quán)限外拌汇,還可以在判斷表達(dá)式中加入運(yùn)行時(shí)狀態(tài)做更多樣性的判斷,而且可以充分利用v-if響應(yīng)數(shù)據(jù)變化的特點(diǎn)颗搂,實(shí)現(xiàn)動態(tài)視圖控制担猛。

具體實(shí)現(xiàn)細(xì)節(jié)參考基于Vue實(shí)現(xiàn)后臺系統(tǒng)權(quán)限控制中的相關(guān)章節(jié)。

自定義指令

v-if的響應(yīng)特性是把雙刃劍丢氢,因?yàn)榕袛啾磉_(dá)式在運(yùn)行過程中會頻繁觸發(fā)傅联,但實(shí)際上在一個(gè)用戶會話周期內(nèi)其權(quán)限并不會發(fā)生變化,因此如果只需要校驗(yàn)權(quán)限的話疚察,用v-if會產(chǎn)生大量不必要的運(yùn)算蒸走,這種情況只需在視圖載入時(shí)校驗(yàn)一次即可,可以通過自定義指令實(shí)現(xiàn):

//權(quán)限指令
Vue.directive('has', {
  bind: function(el, binding) {
    if (!Vue.prototype.$_has(binding.value)) {
      el.parentNode.removeChild(el);
    }
  }
});

自定義指令內(nèi)部仍然是調(diào)用全局驗(yàn)證方法貌嫡,但優(yōu)點(diǎn)在于只會在元素初始化時(shí)執(zhí)行一次比驻,多數(shù)情況下都應(yīng)該使用自定義指令實(shí)現(xiàn)視圖控制。

請求控制

請求控制是利用axios攔截器實(shí)現(xiàn)的岛抄,目的是將越權(quán)請求在前端攔截掉别惦,原理是在請求攔截器中判斷本次請求是否符合用戶權(quán)限,以決定是否攔截夫椭。

普通請求的判斷很容易掸掸,遍歷后端返回的的資源權(quán)限格式,直接判斷request.method和request.url是否吻合就可以了蹭秋,對于帶參數(shù)的url需要使用通配符扰付,這里需要根據(jù)項(xiàng)目需求前后端協(xié)商一致,約定好通配符格式后仁讨,攔截器中要先將帶參數(shù)的url處理成約定格式羽莺,再判斷權(quán)限,方案中已經(jīng)實(shí)現(xiàn)了以下兩種通配符格式:

1. 格式:/resources/:id
   示例:/resources/1
   url: /resources/**
   解釋:一個(gè)名詞后跟一個(gè)參數(shù)洞豁,參數(shù)通常表示名詞的id
   
2. 格式:/store/:id/member
   示例:/store/1/member
   url:/store/*/member
   解釋:兩個(gè)名詞之間夾帶一個(gè)參數(shù)盐固,參數(shù)通常表示第一個(gè)名詞的id

對于第一種格式需要注意的是,如果你要發(fā)起一個(gè)url為"/aaa/bbb"的請求丈挟,默認(rèn)會被處理成"/aaa/**"進(jìn)行權(quán)限校驗(yàn)闰挡,如果這里的”bbb”并不是參數(shù)而是url的一部分,那么你需要將url改成"/aaa/bbb/"礁哄,在最后加一個(gè)”/“表示該url不需要轉(zhuǎn)化格式长酗。

攔截器的具體實(shí)現(xiàn)見App.vue中的setInterceptor()方法。

如果你的項(xiàng)目還需要其他的通配符格式桐绒,只需要在攔截器中實(shí)現(xiàn)對應(yīng)的檢測和轉(zhuǎn)化方法就可以了夺脾。

演示及說明

演示說明:
DEMO項(xiàng)目中演示了動態(tài)菜單、動態(tài)路由茉继、按鈕權(quán)限咧叭、請求攔截。

演示項(xiàng)目后端由rap2生成mock數(shù)據(jù)烁竭,登錄請求通常應(yīng)該是POST方式菲茬,但因?yàn)閞ap2的編程模式無法獲取到非GET的請求參數(shù),因此只能用GET方式登錄,實(shí)際項(xiàng)目中不建議仿效婉弹;

另外登錄后獲取權(quán)限的接口本來不需要攜帶額外參數(shù)睬魂,后端可以根據(jù)請求頭攜帶的token信息實(shí)現(xiàn)用戶鑒權(quán),但因?yàn)閞ap2的編程模式獲取不到headers數(shù)據(jù)镀赌,因此只能增加一個(gè)”Authorization”參數(shù)用于生成模擬數(shù)據(jù)氯哮。

測試賬號:

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

演示地址:

https://refined-x.com/Vue-Access-Control/

轉(zhuǎn)載:https://refined-x.com/2017/11/28/Vue2.0用戶權(quán)限控制解決方案/

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市商佛,隨后出現(xiàn)的幾起案子喉钢,更是在濱河造成了極大的恐慌,老刑警劉巖良姆,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件肠虽,死亡現(xiàn)場離奇詭異,居然都是意外死亡玛追,警方通過查閱死者的電腦和手機(jī)税课,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來豹缀,“玉大人伯复,你說我怎么就攤上這事⌒象希” “怎么了啸如?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長氮惯。 經(jīng)常有香客問我叮雳,道長,這世上最難降的妖魔是什么妇汗? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任帘不,我火速辦了婚禮,結(jié)果婚禮上杨箭,老公的妹妹穿的比我還像新娘寞焙。我一直安慰自己,他們只是感情好互婿,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布捣郊。 她就那樣靜靜地躺著,像睡著了一般慈参。 火紅的嫁衣襯著肌膚如雪呛牲。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天驮配,我揣著相機(jī)與錄音娘扩,去河邊找鬼着茸。 笑死,一個(gè)胖子當(dāng)著我的面吹牛琐旁,可吹牛的內(nèi)容都是我干的涮阔。 我是一名探鬼主播,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼旋膳,長吁一口氣:“原來是場噩夢啊……” “哼澎语!你這毒婦竟也來了途事?” 一聲冷哼從身側(cè)響起验懊,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎尸变,沒想到半個(gè)月后义图,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡召烂,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年碱工,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片奏夫。...
    茶點(diǎn)故事閱讀 38,566評論 1 339
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡怕篷,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出酗昼,到底是詐尸還是另有隱情廊谓,我是刑警寧澤,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布麻削,位于F島的核電站蒸痹,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏呛哟。R本人自食惡果不足惜叠荠,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望扫责。 院中可真熱鬧榛鼎,春花似錦、人聲如沸鳖孤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽淌铐。三九已至肺然,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間腿准,已是汗流浹背际起。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工拾碌, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人街望。 一個(gè)月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓校翔,卻偏偏與公主長得像,于是被迫代替她去往敵國和親灾前。 傳聞我的和親對象是個(gè)殘疾皇子防症,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,440評論 2 348

推薦閱讀更多精彩內(nèi)容