基於 mongodb 設計靈活後台系統管理權限
mongodb 是一款基於 文檔 結構的 nosql 資料庫,目前社區比較火,在文檔的儲存靈活性有著天然的優勢(同集合可以任何形式的行資料,當然我們不會這樣去爛用這種靈活性 :) ),且有不俗的效能表現,以及複本集的高可用!
此款許可權粒度存在範圍與個體的概念,可以理解為 功能性許可權 和 資料歸屬性許可權。資料許可權是功能許可權的再次細分,作為子集。下面,我會一點點剖析,下面的案例會給出所有表結構,和關鍵操作的 sql 語句(基於 golang 語言的 (mgo mongodb driver))和設計圖。當然這可能並不是最佳實務,只是我自己設計的一點心得,已便解決實際問題。
背景
之前在做一款早期 serverless 架構的雛形(現在只能算模版系統 :) )後台管理系統的時候,公司的人員結構很穩定,又為了快速內部上線提供使用,我們想到一款最簡單的基於部門實現泛許可權的功能許可權管理,意思就是,這個部門下面的組擁有這個組建立的所有查看許可權,這個組擁有的功能許可權由角色定義,如果為資深開發人員角色,那麼管理員會分配 模版編輯許可權,模版清緩衝/發布等一套可以從代碼web ide 上編輯到外網訪問發布的整個流程許可權,初級可能只有編輯許可權等。
但是我們很快發現問題,公司雖然有統一的許可權系統,但是控制粒度太粗,所以開始我們是把人員的許可權放入到自己的庫裡維護,時隔幾月,隨著公司的大舉擴張,人員結構,部門等發生翻天覆地的變化,人事變更意味著之前基於部門設計的許可權變成了髒資料,部門之間的資料沒法自動實現訪問,只能通過我們簡單的資料許可權操作機制,讓某一人擁有多個部門的許可權,這樣處理因為不方便的原因可能存在越權處理,因為在 A 部門你擁有發布許可權, 在 B 部門我只想讓你有查詢功能等,這樣有一段時間我們就陷入了不斷為人調整許可權的售後工作!而且在跨部門處理問題都會面臨不方便。
結構圖
思考
當時我們面臨一個元素種類比較多的後台系統,且許可權組合種類比較多,那麼怎樣能夠設計一款能夠自由組合,能夠自驅玩轉許可權,許可權的下方不再由單人控制,以及在做工作交接的時候許可權與資源能夠一鍵交接。所以列了如下幾點特性。
- 許可權控制下放,開發人員不再費力維護許可權。
- 許可權有角色管理控制,有資料許可權的細分概念。
- 許可權以及資源能夠一鍵轉讓。
- 許可權基於人為個體,細分到,某個人下的某條資料。
- 許可權可以臨時組成,不同開發人員,可以同時擁有一套或多套許可權,方便跨部門協作。
- 使用要簡單,與使用者基本資料隔離
- 效能要出眾。可以做成 web 服務 / sdk
實踐
接下來就是進行表結構的梳理,所有結構都會給出 最簡潔關鍵字段及註解
要想細分到資料許可權,那麼人/組必須要有一個元素與資源進行綁定,在此我們用 signkey 作為一種簽名,某個人擁有這個簽名就有對這個資源有一定的操作許可權。一定要有中間簽名的紐帶來建立聯絡,不然資料量的儲存,轉讓,變動都會出現問題。 如果說把資源屬於哪些人來處理的話,可能一個欄位就有很多工號來識別。
所以我們許可權表裡面的 user 資訊表這樣設計
type UserDoc struct { Name string // 姓名 UserId string // 工號 唯一標識 SignKey map[string]string // 簽名 key是簽名 24字元id mongodb _id (可換成任意 唯一 字串),value是簽名的描述。}
可以看到一個人自己可以建立多個私人 簽名
GroupName+UserId 作聯合唯一索引
那麼我們的角色許可權是怎麼設計的呢?一個角色擁有多種職能,我們把每一種職能看作一個介面實現了不同的方法,那麼角色許可權,就是由一個或多個介面實現的組合,介面實現又體現在 url + method 上面,然而問題是,有些介面涉及到操作資料,對資料有寫操作,有的介面,只是單純的某一次事件,不涉及到任何資料查詢和變更。那麼在這裡就會劃分此介面是否開啟對資料做認證。通過不同的介面實現組合,這樣就擁有了角色許可權的維護。
type RoleDoc struct { RoleName string // 角色名稱 Desc string // 角色描述 IsDefault bool // 是否為預設角色,就是使用者登陸進來內建的角色許可權 UserIds []string // 角色下面有哪些使用者, 這裡是一個最佳化點,可以使用外鍵表關聯。 視系統大小可以調整 PathsDataVerify map[string]bool // 該介面是否開啟資料驗證許可權,用於 sql 快速查詢資料 PathMethods []PathMethod // 該角色有哪些功能即 API 的組合, 詳細資料,是一份冗餘資料,方便配置時使用 通過方法把詳細資料轉化為快速查詢資料 Typ int // 角色類型,用於做特權處理, 例如 超級管理員角色,無需驗證許可權 則 type = 0}// restful API 風格設計 一個 path 有多個 method,// method 使用 int 代替 GET=1 POST=2 PUT=4 DELETE=8 HEAD=16... 可以用到 mongodb 位元運算// 如果一個 path 的 get post 方法都賦予了這個角色, 則 MethodInt=3type PathMethod struct { Path string // 請求路徑 MethodInt int // 方法的 int 和 9 代表 GET 和 DELETE 方法}
上述表結構 PathsDataVerify key="1_/template" value=true 因為 1 表示 GET 所以這種表示描述就為 模版的查詢方法,需要開啟資料認證許可權
roleName+Typ 作聯合唯一索引
有了 Path 與角色的關聯,就自然會有 Path 的詳情,以方便管理,作為組成角色的許可權的源,同時可以設定該介面是否開啟資料驗證許可權。
type RouterDoc struct { Path string // 請求路徑 /template Desc string // 路徑實現功能的大類, 例如 模組的 CRUD MethodMap map[string]MethoedDetail // key 方法 具體表現 "1" "2" ..}type MethoedDetail struct { DataVerify bool // 該方法是否開啟 資料驗證 Desc string // 模版的刪除 則 MethodMap key = 8}
通過上面的簡單表述,就可以建立 url + method 等於方法的實現,可以很方便的進行許可權管理
Path 作唯一索引
那麼我們的資料許可權驗證又是怎麼實現的呢。它與角色許可權又是什麼樣的關係。與人又怎麼綁定的,註解如下
type SignDoc struct { SignKey string // 簽名, 某人的私人簽名 + 使用者唯一標識 可以理解成 這個人擁有這個簽名的哪些資料許可權 CreateUserId string // 簽名的建立者 UserId string // 這個成員也擁有這個簽名的一部分/全部許可權, 相當於建立者把自己的簽名共用出,用於多人使用 RouterMap map[string]int // key=Path value=Method $bitsAllSet 計算 {"/template":14} 表述 這個人擁有此簽名下面的模版資料 增/改/刪的許可權}
SignKey+UserId 作聯合唯一索引
這樣建立者可分配的許可權為自己的角色許可權的子集,可以把一個私人key 暴露成公用的授權個他人,那麼他人就對此簽名下面所有的資源,都有相應你配置的許可權,RouterMap 代表他擁有哪些許可權,這樣你就可以把自己的簽名,分給不同的人,不同的人,擁有對此簽名不同的許可權。完全實現了,自己的資料自己做主管理。用於多種協調工作的情境。比如臨時有一個修改活動邏輯的任務,有3個人做,那麼他們組負責人可以建立一個公用的 key ,賦予這三個人,用於此次任務的所有許可權操作。
所有的表結構都註解了,那麼它們是怎麼關聯協作的,下面我們將從一個使用者的角度使用許可權系統裡面所涉及的 sql 語句做簡單解析,以便熟悉整個請求的流轉。
配置許可權流程
初始化路由 Method 描述以及是否開啟資料許可權 ---> 建立角色 ---> 把才建立好的路由與建立的角色進行綁定 ---> 為角色添加使用者 ---> 角色建立自己的簽名 ---> 每一次請求攜帶上籤名 ---> 建立資源的時候攜帶上此簽名與資源綁定
查看個人自己擁有什麼許可權
// userid 查詢 RoleDoc 然後把所有返回的 PathMethods 做彙總 // 通過 PathMethods 也可以結合 RouterDoc 查詢具體的資料驗證許可權
查看別人授與權限
// userid 查詢 SignDoc 然後對返回,做彙總即可
在許可權的配置和管理上面,沒有任何複雜的 sql ,當然在實際項目當中有很多的 sql 組合來滿足不同的配置方式。總體下來是很方便的。
複雜分配舉例,以對資源的 CRUD 為例
- u1 u2 u3, u1 擁有 u2 簽名 u2-s-1 下的 ceph資源讀取許可權, u2 擁有 u1-s-3 下的 template CRU 許可權,u3-s-1 下的ceph CU 許可權。 u3擁有 u1-s-3 所有資料的 R 許可權
資料儲存表現
// RoleDoc[ {"userId":"u1", "signKey":{"u1-s-3":"ceph kv manager"}}, {"userId":"u2", "signKey":{"u2-s-1":"template manager"}}, {"userId":"u3", "signKey":{"u3-s-1":"template manager"}}]
// RouterDoc[ {"path":"/ceph","methodMap":{ "1":{"dataVerify":true,"desc":"query ceph value"}, "2":{"dataVerify":true,"desc":"create ceph value"}, "4":{"dataVerify":true,"desc":"update ceph value"}, "8":{"dataVerify":true,"desc":"delete ceph value"}, } }, {"path":"/template","methodMap":{ "1":{"dataVerify":true,"desc":"query template value"}, "2":{"dataVerify":true,"desc":"create template value"}, "4":{"dataVerify":true,"desc":"update template value"}, "8":{"dataVerify":true,"desc":"delete template value"}, } }]
// RoleDoc 此角色沒有刪除許可權[ {"reoleName":"ceph&template manager","userIds":["u1","u2","u3"], "pathMethods":[ {"path":"/ceph","methodInt":7}, {"path":"/template","methodInt":7} ]}]
// SignDoc 簽名資料許可權關聯[ {"signKey":"u1-s-3","createUserId":"u1","userId":"u2","routerMap":{"/template":7}}, {"signKey":"u3-s-1","createUserId":"u3","userId":"u2","routerMap":{"/ceph":6}}, {"signKey":"u2-s-1","createUserId":"u2","userId":"u1","routerMap":{"/ceph":1}}, {"signKey":"u1-s-3","createUserId":"u1","userId":"u3","routerMap":{"/template":1,"/ceph":1}}]
當資料量特大的嵌套文檔,都可以外鍵單獨行儲存。
驗證許可權
使用者(統一單點)登陸後 ---> 判斷是否為初始使用者(預設角色) ---> 返回角色類型,擁有哪些許可權(前端可以根據擁有的許可權對 ui 進行渲染控制)
使用者請求 攜帶 userid + signkey 請求擷取 path + method (路由如果支援 動態路由, 用 httprouter 解析成後台配置的路由)
使用 userid + path + method 查詢 roleDoc 表 驗證是否存在角色許可權(如果角色為超級管理員,則後面無需驗證)
根據角色許可權 PathsDataVerify 欄位 value 值可以判斷是否驗證資料許可權。
如果需要判斷資料許可權,則 攜帶 userid + signkey + path + method 查詢 signDoc 表,是否存在 有許可權則把相應的 許可權資訊寫入 context 供後續方法使用
如果是查詢介面 使用者無需攜帶 signkey , 需要查詢的 signkey 由許可權處理中間方法計算群組成 signkey 數組 , mongodb 使用 $in 查詢資料
signkey 數組: 自己的私人 key , 別人授權的資料許可權 key 通過 userid + path + method 查詢到所有授權的 signDoc 把裡面的 signkey 放到數組裡
總結
這一套許可權驗證是解決之前固有依賴部門設計的許可權而提出的,此許可權不存在與其他的依賴,只取決與簽名與人與路由之間的綁定關係。可以任意組合!
資料許可權是角色許可權的子集,必須要滿足角色許可權。比如 u1 沒有刪除 /template 的角色許可權, u2 授權 u1 擁有 u2-s-1 的 /template 的刪除許可權,也是不可以的。
通過這套許可權把之前的資料進行了拆分,結構便於理解。當然此套表結構也是適用於其他資料庫的。如果對效能有要求是可以用 redis 對許可權做緩衝的。當然目前我們基於200 + 使用者, 200+ 路由配置,許可權過濾和所有的管理結構都是在 20ms 下的。對於後台系統是可以接受的。
設計不好,僅供參考