RBAC(基于角色的權(quán)限控制)是一個(gè)老話題了,但是這兩天我試圖設(shè)計(jì)一套表結(jié)構(gòu)實(shí)現(xiàn)完整的RBAC時(shí),發(fā)現(xiàn)存在很多困難。
我說(shuō)的完整的RBAC,是指支持角色樹(shù)形結(jié)構(gòu)和角色分組。具體來(lái)說(shuō),應(yīng)當(dāng)包含如下權(quán)限控制需求:
- 父級(jí)角色可以訪問(wèn)甚至是修改其子級(jí)的數(shù)據(jù),包含直接子級(jí)直到最終子級(jí)。
- 角色可以訪問(wèn)其所在組的數(shù)據(jù)。
- 父級(jí)角色可以訪問(wèn)其所有子級(jí)(從直接子級(jí)到最終子級(jí))所在組的數(shù)據(jù)。
而具體到我的系統(tǒng)中,還應(yīng)當(dāng)有如下需求。
- 兼容多種數(shù)據(jù)庫(kù)產(chǎn)品。只能用簡(jiǎn)單的表,視圖,存儲(chǔ)過(guò)程和函數(shù)等實(shí)現(xiàn)。
- 同時(shí)兼容單條數(shù)據(jù)處理和批量數(shù)據(jù)處理的需求。
且不論這些具體需求,RBAC的基本表應(yīng)當(dāng)如下四個(gè):
- roleList表,記錄所有的角色和角色組。
- roleId: PK, 角色/組的ID,全局唯一,不區(qū)分角色和組。
- roleName:角色/組的名稱。
- roleType: R - 角色,G - 組
- rolePermission表,記錄每一個(gè)角色/組對(duì)每一個(gè)對(duì)象的權(quán)限。
- permissionID: PK, 無(wú)特定意義。
- role: 角色/組的ID。
- object: 對(duì)象的ID。
- permission: 權(quán)限標(biāo)識(shí),如讀,寫(xiě),刪等。
- roleRelationship表,記錄角色/組之間的關(guān)系。
- relationId: PK, 無(wú)特定意義。
- superiorRole: 父角色/組的ID。
- role:子角色,子組,成員角色,成員組的ID。
- relationship: 關(guān)系標(biāo)識(shí),可在如下設(shè)置集中選取一個(gè)。
- PG標(biāo)識(shí):P - 父子關(guān)系,G - 組/成員關(guān)系。
- PPGG標(biāo)識(shí):在PG集上,再加三種:PP - 間接父級(jí)關(guān)系,GG - 組內(nèi)組關(guān)系,CG - parentRole是組,childRole的子角色或間接子角色是其成員,或其子組(含間接子組)的成員
- objectList表,記錄所有的對(duì)象。
- objectId: PK,對(duì)象ID,全局唯一。
- objectName: 對(duì)象名稱。
- ... ...
分析上述表結(jié)構(gòu),不難發(fā)現(xiàn),問(wèn)題的關(guān)鍵在于從rolePermission表中讀取數(shù)據(jù)時(shí),如何限定角色/組的范圍.
方案一
如果角色和組的總量不大,比如在100以內(nèi),采用PPGG標(biāo)識(shí)關(guān)系,讀取數(shù)據(jù)時(shí)是最快的。這個(gè)時(shí)候的SQL只需要一個(gè)輸入?yún)?shù)?roleId:
SELECT object FROM rolePermission p left join roleRelationship r on p.role = r.role WHERE p.role = ?roleId or r.superiorRole = ?roleId. (尚未驗(yàn)證SQL的正確性)
但是,這個(gè)方案是以極度冗余roleRelationship表的數(shù)據(jù)為代價(jià)的,比如有100個(gè)角色,那么roleRelationship中將會(huì)有100 * 100 =10,000條記錄。而在每次調(diào)整角色和R角色組的時(shí)候,就要在roleRelationship中一次增加或刪除100條記錄。這個(gè)開(kāi)銷(xiāo)是比較大的。
方案二
只標(biāo)識(shí)PG,查詢時(shí)接收的輸入?yún)?shù)為一個(gè)完整的相關(guān)角色列表?roleList。
SELECT object FROM rolePermission WHERE role in (?roleList)
在系統(tǒng)運(yùn)行時(shí),這個(gè)?roleList通常可以從role hierarchy cache中取到,比較方便。這個(gè)方案的主要問(wèn)題有二:
1)如果?roleList過(guò)長(zhǎng),使用in判斷性能會(huì)很差。
2)在有些情況下,如報(bào)表查詢和系統(tǒng)外查詢時(shí),取得roleList不太方便。
方案三
只標(biāo)識(shí)PG,但使用如下三個(gè)數(shù)據(jù)庫(kù)函數(shù)來(lái)判斷角色/組之間的關(guān)系。
- boolean isChild(role, parentRole) - 如role為parentRole的子,返回true。
- boolean isDescendant(role, ancestorRole) - 如role為ancestorRole的子或間接子級(jí),返回true。
- boolean isMember(role, group) - 如role為group的成員或子組的成員,返回true。
- boolean descendantIsMember(role, group) - 如role的子或間接子級(jí)為group的成員,返回true。
- boolean isBelong(role, super) - 如role為super的子,間接子,成員或間接員,或者role的子(含間接子)是super的成員或子組成員,返回true。
在查詢時(shí),也只需要接收一個(gè)?roleId:SELECT object FROM rolePemission WHERE isBelong(?roleId, role)
如何寫(xiě)出高性能的數(shù)據(jù)庫(kù)函數(shù)是實(shí)現(xiàn)這個(gè)方法的關(guān)鍵。
上述方法僅是理論分析,我傾向于方案二。
終于想到新的方案了。
方案四,
結(jié)合方案一和方案二,在roleRelationship中,對(duì)前兩級(jí)(也可以是三級(jí)或四級(jí))角色,保存其所有的下級(jí)角色和組。這樣,如果以前兩級(jí)角色查詢數(shù)據(jù),就使用方案一,如果以第三級(jí)及以下的角色查詢數(shù)據(jù),就使用方案二。
仍以100個(gè)角色為例,每個(gè)角色要保存三個(gè)關(guān)系:一級(jí)主管角色,二級(jí)主管角色,直接主管角色,最多有300條數(shù)據(jù)。
每往角色組中加一個(gè)角色,也需要加入三條數(shù)據(jù):角色本身,一級(jí)主管角色,二級(jí)主管角色。
但往角色組中加一個(gè)子組,需要加入的數(shù)據(jù)量就大一些:子組本身,子組所有角色,子組所有角色的一級(jí)主管角色和二級(jí)主管角色。如在多個(gè)子組中發(fā)現(xiàn)同一角色,可重復(fù)保存,但應(yīng)在表中附加說(shuō)明是由哪個(gè)子組導(dǎo)入的。這樣在刪除子組時(shí)就可以有選擇性的刪除。
但重復(fù)子組的情況就比較麻煩,還有等考慮。假充有組g01,g11,g12,g21。g01包含g11和g12,g11和g12分別包含g21。從g01中刪除g11時(shí),如何判斷g21的去留?看來(lái)還是應(yīng)當(dāng)在維護(hù)時(shí)判斷應(yīng)不應(yīng)當(dāng)刪除。