Vue 實現前端權限控制
阿新 • • 發佈:2018-10-25
角色 -- 插入 示例 tails headers 悖論 如果 管理 為什麽做前端權限控制前端權限控制並不是新生事物,早在後端 MVC 時代,web 系統中就已經普遍存在對按鈕和菜單的顯示 / 隱藏控制,只不過當時它們是由後端程序員在 jsp 或者 php 模板中實現的。隨著前後端分離架構的流行,前後端以接口為界實現開發解耦,權限控制也一分為二,前端權限控制的所有權才真正回到了前端。可能有的同學會想,前後端分別做一套控制,是不是將事情復雜化了,而且從根本上講前端沒有秘密,後端才是權限的關鍵,那是不是只在後端做控制就可以了。對於這個問題我們首先應該明確,前後端權限控制他們的控制對象、控制目的和控制手段都不一樣,如果僅從技術實現的角度講,確實只在後端做控制就足夠了,但在實際項目中,前端權限控制也有其不可或缺的作用,主要體現為三點:提升突破權限的門檻;過濾越權請求,減輕服務端壓力;提升用戶體驗。第一點可以理解為前端權限控制是系統安全的排頭兵,雖然不是主力,但起碼手動輸 url、控制臺發請求、開發者工具改數據這種級別的入侵可以防範掉;第二點是為了省錢,不該發的請求幹脆就讓他發不出去,帶寬都是錢買的;第三點是從用戶體驗角度出發,一個設計優良的系統理應根據權限為每個用戶展現特定的內容,避免在界面上給用戶帶來困擾,這是前端的本職工作,也是我個人做前端權限最大的動力之一。前端權限控制具體指什麽前端權限歸根結底是請求的發起權,請求的發起可能由頁面加載觸發,也可能由頁面上的按鈕點擊觸發。總的來說,所有的請求發起都觸發自前端路由或視圖,所以我們可以從這兩方面入手,對觸發權限的源頭進行控制,最終要實現的目標是:路由方面,用戶登錄後只能看到自己有權訪問的導航菜單,也只能訪問自己有權訪問的路由地址,否則將跳轉 4xx 提示頁;視圖方面,用戶只能看到自己有權瀏覽的內容和有權操作的控件;最後再加上請求控制作為最後一道防線,路由可能配置失誤,按鈕可能忘了加權限,這種時候請求控制可以用來兜底,越權請求將在前端被攔截。怎麽做前端權限控制控制的第一步是知道用戶擁有哪些權限,所以用戶登錄後第一件事是獲取權限數據。權限數據至少應該包括路由權限和資源權限。路由權限顧名思義,就是用戶可訪問的路由集合,以此作為設置前端路由和生成導航菜單的依據;資源權限是用戶可訪問的資源集合,“資源” 概念來自 RESTful 架構,如果對 “資源” 感到陌生也可以簡單理解成用戶能夠發起的所有請求集合,以此作為視圖控制和請求攔截的依據。這裏插入講一下 “角色” 這個概念,可能有的系統會通過角色來做權限控制,我理解的角色就是特定幾個資源打包後的快捷方式。比如擁有總經理這個角色意味著擁有 a,b,c 這三個資源,副總經理就只有 b,c 兩個資源,為用戶賦予角色的本質是為用戶賦予角色背後的資源。引入角色這個概念的好處是,後臺可以通過賦角色的方式,很方便的為某一類用戶賦予特定的資源集合,而角色的作用應該僅限於此,尤其不應該將角色用做前端權限控制的依據,因為角色背後的資源權限是後端動態可配的。我們也可以創建一個名字叫做 “總經理” 的角色,但其實一個資源都沒有,所以前端應該始終關註資源權限本身,而只將角色視為用戶的一個普通屬性就好了。有了權限數據下一步就是分別-實現對路由、視圖、請求的控制。路由控制首先要實現動態菜單,這樣就可以對常規訪問方式進行限制;對於非常規訪問方式比如手動修改 url,可以從前端路由處著手做控制。路由控制的思路有兩種,一種是初始化即掛載全部路由,每次路由跳轉前做校驗;另一種是只掛載用戶擁有的路由,相當於從源頭上做了控制。前者的缺點很明顯,每次路由跳轉都要做一遍校驗是對計算資源的浪費,另外對於用戶無權訪問的路由,理論上就不應該掛載。後者解決了上述問題,但仔細想這裏存在一個悖論,要按需掛載路由就需要知道用戶的路由權限,要知道用戶的路由權限就需要用戶先登錄進來,但路由沒有加載應用也沒有初始化,用戶從哪兒登錄?這裏又可以有兩種解決思路,一種是單獨做一個登錄頁,登錄後帶著用戶憑據跳轉到前端應用;另一種是先初始化一個只有登錄路由的應用,用戶登錄後動態添加路由,當然這需要框架提供支持。視圖控制需要實現一個可以在視圖層調用的權限驗證方法,輸入用戶期望的權限,輸出是否擁有該權限,將調用這個方法的結果,作為界面上需要驗證權限的控件或元素顯示與否的依據。請求控制實際上就是為你使用的 HTTP 庫實現一個請求攔截器,對將要發起的請求與用戶資源權限進行匹配,攔截越權請求。這裏值得一提的是對於攜帶參數的 url,需要先進行模式約定,比如/people/1這個 url 可以在權限中描述為/people/**,那麽攔截器中就要先將這種 url 處理成約定後的格式,然後再進行權限驗證。基於 Vue 的實現方案概述到目前為止我們談的都是脫離具體技術棧的實現思路,理論上可以用任何技術棧實現這個思路,但我在項目中用的是 Vue,所以下面介紹的實現細節全部基於 Vue。先來看整個流程:從第一步 “初始化 Vue 實例” 到 “獲取權限數據” 之間做的其實是用戶鑒權,這一步跟權限控制關系不大,怎麽做都可以。這裏的做法是用戶登錄後獲得一個 token,然後在請求 Headers 中設置 “Authorization”。token 會存進 sessionStorage 裏,用戶刷新將直接使用本地 token 授權,並重新獲取權限數據,如果本地 token 失效,那麽後端應該返回 401 狀態碼,前端跳回登陸界面。從 “獲取權限數據” 到 “異步加載路由組件” 之間做的是用戶權限初始化,分別用addRoutes()方法實現動態路由及菜單,實現全局權限驗證方法及指令,以及實現 axios 請求攔截。因為用的是動態路由方案,當動態路由註入時異步路由組件會開始加載,首次訪問通常是加載首頁組件,如果是用戶刷新,地址欄還保留著之前瀏覽的的 url,那麽動態路由註入後也會正確的加載對應的路由組件,顯示對應的界面。下面我們著重來看權限初始化部分的實現細節,因為所有的初始化操作都基於後端給的權限數據,所以我們先來約定權限數據的數據格式:路由權限數據是如下格式的對象數組資源權限數據是如下格式的對象數組路由控制動態路由最初實例化的路由裏僅包含登錄和 404 之類的基本路徑,而我們期待完整的路由是這樣的:一級路由只增加了一個首頁,以及最後兜底的 404,其他功能模塊都作為首頁的子路由,這麽做主要是為了可以在首頁實現全局導航菜單,實際項目中也可以調整這個路由結構。下一步我們關註的重點應該是獲取首頁的子路由們,思路是事先在本地存一份整個項目的完整路由數據,根據用戶的路由權限對完整路由進行篩選。具體說一下篩選的實現,先將路由權限數據處理成如下結構:然後遍歷本地完整路由,在循環中將路徑拼接成上述結構中的 key 格式,通過hashMenus[route]判斷路由是否匹配。如果你有更好的篩選方法,或者後端返回的路由權限數據與約定不同,也可以酌情修改這部分的邏輯,只要最終能得到可用的路由數據就可以。註意在調用addRoutes()方法時,404 頁面的模糊匹配一定要放在數組的最後,否則其後的路由都不會生效。動態菜單用戶的實際路由數據可以直接用來生成導航菜單,但首先有一個小問題,路由數據是在根組件中得到的,而導航菜單存在於首頁組件中,我們需要用某種方式將菜單數據傳遞到首頁。方法有很多,考慮到菜單數據在整個用戶會話過程中不會發生改變,而且除了生成菜單之外就沒有其他共享價值了,所以這裏就用了最簡單直接的辦法,把菜單數據掛在根組件上,在首頁裏用this.$parent.menuData獲取。另外,導航菜單很可能會有一些個性化需求,比如添加欄目圖標,這可以通過在路由中添加meta數據實現,例如將圖標 class 或 unicode 存到路由 meta 裏,模板中就可以訪問到 meta 數據,用來生成圖標標簽,類似的需求也都可以這樣來做。另一個問題可能在多角色系統中比較常遇到,就是當不同角色都有一個名字相同但功能不同的路由,會發生路由名稱沖突。舉例來說, 系統管理員和企業管理員都有一個叫做 “ 賬號管理 “ 的路由,但他們的操作對象不同,實際上這就是兩個完全不同的路由,所以路由的 name 肯定要有所區分。為了能在前端導航菜單上都能顯示 “ 賬號管理 “ 這個名字,我們可以為路由再起一個別名,放進meta.name,生成導航菜單時優先展示別名就可以了。視圖控制全局驗證方法驗證方法的的實現本身很簡單,全局混入一個$_has()方法,內部實現無非是將所需權限與擁有權限做比對,返回一個布爾值。重點在於工程實踐上的優化,怎麽能讓這件事做起來更方便,通常的做法可能是下面這樣的:像這樣的按鈕一個頁面上可能有多個,每個頁面都需要手動的去維護權限信息,而且過程中還要頻繁的在模板和腳本之間、當前組件文件和 api 文件之間來回切換,去查閱每一個權限對應資源的 url 和方法具體是什麽。這樣的流程顯然非常容易出錯,開發體驗也很不好。經過摸索和總結,最終使用的方案是將權限信息和請求 api 維護在一起,組成一個資源對象,驗證方法接收資源對象為參數,方法內部自動獲取對象中的權限信息用做驗證。這樣做的好處是在寫資源的請求方法時可以順手維護上資源的權限信息,這樣一來在前端模板中就不需要出現具體的權限信息,只要給到這個資源對象的名稱就行了,另外權限驗證方法應該允許多個權限聯合驗證,所以將參數格式改成數組。最終用法是這樣的:資源對象示例:驗證方法的實現比較簡單就不展開了,將權限驗證方法全局混入就可以在項目中很容易的配合v-if實現元素顯示控制,v-if這種方式的優點在於除了可以校驗權限外,還可以在表達式中結合業務數據做更多樣性的判斷,從而實現隨業務變化的動態視圖控制。自定義指令v-if的響應特性是把雙刃劍,因為表達式在應用運行過程中會頻繁觸發,但實際上在一個用戶的會話周期內其權限極少會發生變化,v-if產生的大量運算都是不必要的,多數時候我們希望只在視圖載入時做一次校驗決定元素的去留,這個需求可以通過自定義指令實現:自定義指令內部仍然是調用全局驗證方法,但優點在於只會在元素初始化時執行一次,多數情況都應該使用自定義指令實現界面元素的權限控制。請求控制請求控制是利用 axios 攔截器實現的,原理是在請求攔截器中獲取本次請求的 url 和 method 信息,再與資源權限數據做比對,判斷請求是否合法從而決定是否攔截。普通請求很容易處理,遍歷資源權限數據,直接判斷request.method和request.url是否吻合就可以了。對於帶參數的 url 就不能用全文匹配了,而應該用模式匹配,這裏需要前後端先協商一致。後端返回的資源權限數據中,需要將 url 的參數用通配符代替,前端的請求攔截器中也要將帶參數 url 處理成跟後端一致的格式,這樣才能正確校驗這類 url,例如以下這兩種常見的參數格式及其代替寫法:格式的匹配和參數替換可以用正則表達式實現,可能遇到的一個問題是,如果你要發起一個 url 為 “/aaa/bbb” 的請求,默認會匹配為上述第一種格式,然後被處理成 “/aaa/**” 進行權限校驗。如果這裏的 “bbb” 並不是參數而是 url 的一部分,那麽你可以將 url 改成 “/aaa/bbb/“,在最後加一個 “/“ 以繞過格式匹配。如果你的項目還需要其他的通配符格式,只需要在攔截器中實現對應的匹配和轉化方法就可以了。--------------------- 作者:GitChat技術雜談 來源:CSDN 原文:https://blog.csdn.net/gitchat/article/details/78849246 版權聲明:本文為博主原創文章,轉載請附上博文鏈接!
Vue 實現前端權限控制