論後臺管理專案中許可權的設計思想
說到許可權很多人都會想到RBAC,ACL等等,這些方案都是十分成熟的許可權管理方案,最早寫PHP用yii2框架的時候,就自帶了rbac許可權管理,也對rbac比較熟悉,但今天想說的不僅僅侷限於路由許可權。
RBAC許可權管理
關於rbac許可權管理gg可以出一堆文章,基於角色的訪問控制,把一堆路由分配給一個角色,然後把一堆角色分配給專案中的某個人,此人即擁有這些路由的訪問許可權。
這裡只對rbac做出簡單的說明,此處不多說。
現在的矛盾來了,如果兩個人People_A和People_B分別屬於兩個專案組Team_A, Team_B,同時這兩個專案組分別擁有一條資料Data_A, Data_B,此資料有如下兩條路由:
- GET /project/data 查詢資料詳情
- PUT /project/data 修改資料詳情
People_A和People_B都擁有上述兩個路由的許可權,那麼怎麼區分二者只能操作各自組的資料Data_A和Data_B?
這裡答案還是挺簡單的,人與資料同時綁定了專案組,只需要判斷人操作的資料是否同時屬於一個專案組即可!!!
問題是否就這麼簡單呢?
再進一步的需求: People_A需要擁有Team_B的Data_B資料的上述兩條資料許可權,又該如何解決?
這個問題也比較簡單,可以修改我們的業務邏輯。每個人可以屬於多個組,即將People_A加入Team_B組,那麼只需要在做Data_B的許可權判斷是判斷People_A所在的所有組,只要一個組與Data_B是在一個組即可!!!
好開心呀,上述問題都解決了,是否就完了呢?
需求又來了,People_A只能擁有Team_B的Data_B資料的查詢資料詳情路由(GET /project/data)的許可權,即People_A只能檢視Data_B資料,不能修改。 這個需求如何搞?
好像現有的解決方案沒法整呀!
下面進入正題,新鮮出爐的一套許可權解決方案,也是我們專案經過多次許可權的折騰最終總結出來的。
新許可權的特點
- 路由許可權的管理還是使用的RBAC
- 每條路由除了擁有路由許可權,還擁有資料許可權
- 為解決資料許可權,每個使用者可建立一個數據許可權的key,我們叫做signKey
- 專案中啟用資料許可權的路由所操作的資料,都需要分配一個signKey
- signKey的建立者,只需要將他人加入到signKey的授權人中,並再給其分配相關資料許可權的路由
說了上述幾點,可能由於文字功底不足,完全聽不懂,舉個形象的栗子:
signKey就是一個QQ群,建立此signKey的就是群主,群主可以將資料上傳到該群中,然後再拉一些人到此群中,那麼這些人就能夠對這些資料進行操作。同時給每個人分配的路由不一樣,那麼每個人對資料的操作許可權也不一樣,可以控制到部分人能夠訪問和修改資料,部分人只能訪問資料而不能修改資料。
Talk is cheap, show me your code!
下面具體從資料結構層面分析,如下程式碼全部是golang編寫,下述資料結構適用於mongodb的儲存。
路由資料結構
type RouteInfo struct {
Uri string //路由
Desc string //路由的描述
GroupName string //專案組
MethodMap map[string]VerifyData //key是方法對應的數字
}
type VerifyData struct {
Enable bool // 方法是否啟用資料許可權
MethodDesc string // 方法的描述
}
為方便儲存和在進行資料許可權判斷時能夠使用二進位制操作,所有方法全部對應相應的整型值:
GET --> 1
POST --> 2
PUT --> 4
DELETE --> 8
路由表儲存所有專案應該有的路由和方法。
角色資料結構
type RoleInfo struct {
RoleName string // 角色名稱
Desc string // 角色描述
GroupName string // 專案組
IsDefault bool // 預設角色
UserIds []string // 角色的擁有者
RouterMap map[string]bool //key method_uri(: 1_/project/data),value 是否開啟資料許可權
Address []Address // 存一份冗餘資料,在做操作的時候很有用途
Type int //角色的型別
}
type Address struct {
Uri string //路由
MethodValue int //這裡是所有method對應的整型值之和
}
角色表就是用來實現RBAC的,建立角色時將路由表中的路由新增進來,然後再加人,即可實現完整的RBAC功能。但為了判斷資料許可權,RouterMap欄位的value是bool值,如果該路由需要進行資料許可權判斷,那麼此人擁有路由許可權還不能操作資料,還需要進行資料許可權判斷。當然超級管理員無需此約束!!!
使用者資料結構
type UserInfo struct {
Name string // 使用者名稱字
UserId string // 使用者工號
GroupName string // 專案組
SignKey map[string]string //key是signKey,value是signKey的描述
}
使用者表儲存最基本的使用者資訊,其他各類使用者的資訊在專案中進行儲存。SignKey欄位就是使用者建立的signKey。
SignKey資料結構
type SignInfo struct {
SignKey string // 簽名
UserId string // 工號
GroupName string // 專案組
VerifyDataUri map[string]int // key uri, value 是方法的整型值的和
}
SignKey與UserId組成唯一索引,一個SignKey可以分配給多個UserId,但VerifyDataUri的不同,就能夠區分不同使用者擁有不同的資料許可權。
對於SignKey解決資料許可權的栗子:
現有如下三個路由和方法開啟了資料許可權,
GET /project/querydata
(查詢資料),POST /project/updatedata
(修改資料) 和DELETE /project/deletedata
(刪除資料)。現有三個使用者
PeopleA
,PeopleB
,PeopleC
。現
PeopleA
建立了一個signKey
叫做PeopleA_SignKey_1
,同時PeopleA
建立了一條資料Data1
並且綁定了PeopleA_SignKey_1
,那麼自然PeopleA
能夠通過上述三個路由對Data1
資料進行查詢,修改,刪除操作,當PeopleB
和PeopleC
不能操作資料Data1
,因為這三個路由開啟了資料許可權。現
PeopleA
需要讓PeopleB
僅能夠檢視Data1
資料,PeopleC
能夠檢視和修改Data1
資料,該如何操作呢?PeopleA
僅需要將(PeopleA_SignKey_1
、GET /project/querydata
)授權給PeopleB
即可;
PeopleA
需要將(PeopleA_SignKey_1
、GET /project/querydata
和POST /project/updatedata
)授權給PeopleC
即可;
此時即可符合上述需求。上述問題會自發地引出下一個問題,即上述
PeopleA
建立的PeopleA_SignKey_1
的signKey只是自己繫結路由和方法後授權給PeopleB
和PeopleC
的,那麼PeopleA
建立的PeopleA_SignKey_1
能夠讓另外一個人授權給PeopleB
和PeopleC
嗎?答案是肯定的,此時假設授權signKey的路由為
PUT /oreo/auth/grantsign
,並且該路由和方法開啟了資料許可權,現PeopleA
需要PeopleD
的協助,使PeopleD
能夠幫助自己將PeopleA_SignKey_1
授權給他人。此時
PeopleA
只需要將(PeopleA_SignKey_1
、PUT /oreo/auth/grantsign
)授權給PeopleD
即可,此時PeopleD
就可以協助管理PeopleA_SignKey_1
了。此時經過上述操作後
PeopleD
能夠查詢,修改或刪除資料Data1
嗎?答案是否,因為
PeopleA
沒有將相關的路由和方法繫結PeopleA_SignKey_1
授權給PeopleD
。此時經過上述操作後,若
PeopleA
又建立了一個PeopleA_SignKey_2
的signKey,PeopleD
能夠協助管理嗎?- 答案也是否,因為
PeopleA
沒有繫結管理PeopleA_SignKey_2
的路由和方法給PeopleD
。
整個許可權的設計思路和資料結構即說明結束了,下面需要解決另一個問題。
現在許多路由都是動態路由,什麼是動態路由:
- 專案定義路由: GET /project/:name/*path
- 使用者實際訪問請求: GET /project/xkeyideal/usr/local/lib
- 訪問請求是能夠匹配上專案定義的路由
此時的問題就是如何使用者訪問請求能夠成功匹配上專案定義的路由?
解決方案有兩個:
- 簡單粗暴,不允許在專案定義動態匹配的欄位,全部都是明確的,任何引數都通過Query或Body傳參。此方法是最佳方案,不會出錯,也無需大動干戈。
- 必須支援動態匹配欄位(為啥要裝逼???),自己寫動態匹配程式碼,golang有開源庫的實現vestigo
最後簡單給出,我們實現方案簡單的許可權判斷流程圖:
圖上的說明應該能看懂,這裡僅僅是我們的許可權判斷流程,需要採用者,可以結合上述思路進行擴充套件。
結束語
此許可權的設計方案是我們經過兩年來幾次失敗的設計後最新得出來的方案,經過初步的檢驗能夠解決開篇說的那些矛盾和我們專案的一些許可權矛盾,希望對各位讀者有用。