在前后端分離Web項目中,RBAC實現的研究
最近手頭公司的網站項目終於漸漸走出混沌,走上正軌,任務也輕松了一些,終於有時間整理和總結一下之前做的東西。
以往的項目一般使用模板引擎(如ejs)渲染出完整頁面,再發送到瀏覽器展現。但這次項目的處理方式不同,整個項目由前端AngularJS和后端NodeJS進行了前后端的分離。后端Nodejs提供靜態文件服務和API接口,前端則通過AJAX請求調用后端的API,已JSON數據包來進行數據交換。
同時,用戶權限管理方面,我選用了RBAC(基於角色的訪問控制,Role-based access control)最基本的實現來管理(用戶表、角色表、權限表、用戶-角色關聯表、角色-權限關聯表)。而當我實際開發這個模塊的時候,立刻就意識到和以往項目的區別:
在使用后端模板引擎渲染頁面的時候,后端可以根據用戶權限有選擇地渲染一些受限的頁面元素。
而在前后端分離的開發方式下,由於后端並不渲染任何頁面,是否顯示某個頁面元素,完全是由前端來決定的。
對此,我考慮了一種相對簡單的方式:
將用戶最終所具有的所有權限通過API返回給前端,前端根據權限控制頁面元素。
后端本身就知道用戶權限,每個API接口的入口處添加權限的檢查。
然而,這樣的處理方式是有一定局限性的:
后端每個API入口處都必須寫一遍檢查。
僅僅到權限級別,太過寬泛,無法進行粒度更小的控制。
權限雖然可以通過用戶、角色來實現可配置,但是權限與頁面元素、API是否允許調用之間卻是由代碼編寫確定,這部分是無法配置的。
這顯然不是我能做到的最佳解決方案。
至少對於后端,我想要的是一種更加靈活可配置,並對業務代碼透明的權限檢查方式。
於是,我在之前解決方案的基礎上,做了進一步細化,並將頁面元素、API都當作資源來看待。同時,為了前端可以做更加靈活的控制,頁面元素進一步抽象成一份Key-Value數據(我稱之為頁面環境變量ENV),供前端使用。這樣,用戶經過RBAC模塊最終得到的不是權限,而是“允許訪問的API列表(支持*和**通配)”和“環境變量”。
於是,我在權限表之后,又增加了:API路徑表、ENV環境變量表、及相關關聯表。例如:
角色-權限:
角色 | 權限 |
用戶 | 基礎權限 |
管理員 | 管理員基礎權限 |
用戶管理員 | 用戶管理員權限 |
用戶管理員首頁 |
權限-API路徑:
權限 | API路徑 | 說明 |
基礎權限 | /public/** | 所有公共資源 |
/myAccount/** | 個人賬戶所有操作 | |
用戶管理員權限 | /manage/users/do/list | 后台管理用戶列表 |
/manage/users/*/do/get | 后台管理查看任意用戶信息 |
權限-ENV環境變量:
權限 | Key | Value | 說明 |
管理員基礎權限 | ShowManageBtn | 1 | 顯示“管理”按鈕 |
pageSizeForManage | 50 | 后台管理列表頁行數 | |
用戶管理員權限 | ShowManageUsersBtn | 1 | 后台管理顯示“用戶”按鈕 |
用戶管理員首頁 | manageHomePageUrl | /main.html#/manage/users | 后台管理頁面首頁地址 |
其中,用戶最終的API路徑表用於后端API進行權限的檢查。ENV環境變量返回給前端供其控制頁面。
具體而言:
后端只要在請求入口(而不是單個API入口)處,對請求的URL路徑和上面的“允許訪問的API路徑列表”進行比對。請求的路徑如在列表中,則通過檢查,之后的業務邏輯無需再關心權限問題。如不在列表中,則直接返回拒絕請求。
前端則可以直接使用ENV環境變量中的Key-Value來控制頁面,
如配合AngularJS的ng-show: <div ng-show="hasEnv('ShowManageBtn')">...</div>
通過使用這種方式,不僅避免了對每個權限進行 if...else 處理,同時,對后續修改及配置提供了相當大的便利。
當然,這種處理方式也有一些注意點(或者說缺點)。
1. API路徑設置要合理有規律,方便通配(單個星號通配1層路徑、兩個星號通配多層路徑)
統一路徑命名:如管理類接口都由/manage開頭,那么在配置時,只需要使用/manage/**即可通配所有的路徑(包括:/manage/home、/manage/users/list)。
做好分層規划:如管理類接口可以按功能細分 /manage/users/**、/manage/posts/**
以統一的動詞結尾:如只讀權限可以配置為/manage/**/do/get,添加權限可以配置為/manage/**/do/add。
處理對象的ID寫進路徑:如查看用戶信息的路徑為 /manage/users/1/do/get、/manage/users/2/do/get,可以配置權限為/manage/users/*/do/get
2. 避免粒度過細
過細的粒度會導致API路徑以及ENV環境變量數量的暴增。不僅管理起來麻煩,而且也會一定程度上影響權限檢查時的效率。
對於API路徑的粒度,一定要嚴格管理API路徑格式以及盡量使用通配,詳細見上述第1條。
對於ENV環境變量,可以考慮合並一些總是一起出現的內容。如系統只區分讀權限和寫權限,則添加、編輯、刪除按鈕的控制就可以合並為一個 {"btnForWrite": 1} 來處理,而不是為每個按鈕都添加一個ENV環境變量。甚至可以將Value設置為一個JSON來保存多個值 {"canWrite":{"addBtn":1, "editBtn":1}}
3. 設置不進行RBAC認證的API路徑白名單
比如之前例子中的“基礎權限”中的 /public/** 這類肯定允許訪問的路徑,其實是沒有必要特地添加一個權限來管理的。
這類API路徑應當寫到網站配置中,作為不進行RBAC認證的API路徑。
4. 對API路徑及ENV環境變量列表進行緩存
每個請求都查詢數據庫來獲取API路徑列表顯然有點浪費資源。可以針對每個用戶,將API列表及環境變量保存到Session中,這樣只有第一次請求時才會查詢數據庫,之后的請求在RBAC檢查權限時,只需要從Session取回之前緩存的API列表和ENV環境變量即可。但要注意用戶權限在改變時,需要及時令Session中的數據失效。
我在項目中的做法是保存進Redis中,並以 cache@rbac#userId:{userId} 為Key進行保存。當用戶角色發生變化時,按照 cache@rbac#userId:{userId} 清除當前用戶的權限緩存即可;當角色、權限、API路徑、ENV環境變量發生變化時,由於不是很好明確到底影響多少用戶,所以直接按照 cache@rbac#userId:* 來清除所有用戶的權限緩存。
總結:前后端分離的項目中,
1. 后端服務器僅僅暴露API,所以功能級權限管理可以處理為API的訪問控制。
2. 前端頁面需要從后端服務器獲知如何控制頁面元素。同時,直接返回控制方式或具體數值可以減少前端 if...else 數量。
/* * Yiling Zhou * Shanghai, China * -.-- .. .-.. .. -. --. / --.. .... --- ..- * ... .... .- -. --. .... .- .. --..-- / -.-. .... .. -. .- */