1. 程式人生 > >在前後端分離Web專案中,RBAC實現的研究

在前後端分離Web專案中,RBAC實現的研究

在前後端分離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中,並以 [email protected]#userId:{userId} 為Key進行儲存。當用戶角色發生變化時,按照 [email protected]#userId:{userId} 清除當前使用者的許可權快取即可;當角色、許可權、API路徑、ENV環境變數發生變化時,由於不是很好明確到底影響多少使用者,所以直接按照 [email protected]#userId:* 來清除所有使用者的許可權快取。

 

總結:前後端分離的專案中,

1. 後端伺服器僅僅暴露API,所以功能級許可權管理可以處理為API的訪問控制。

2. 前端頁面需要從後端伺服器獲知如何控制頁面元素。同時,直接返回控制方式或具體數值可以減少前端 if...else 數量。