說到權限很多人都會想到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
最後簡單給出,我們實現方案簡單的權限判斷流程圖:
圖上的說明應該能看懂,這裏僅僅是我們的權限判斷流程,需要採用者,可以結合上述思路進行擴展。
結束語
此權限的設計方案是我們經過兩年來幾次失敗的設計後最新得出來的方案,經過初步的檢驗能夠解決開篇說的那些矛盾和我們項目的一些權限矛盾,希望對各位讀者有用。