(本文沿著思路來寫,不會一開始就給出最后結論。)

很多項目需要對用戶操作進行鑒權。它們的需求可以歸納為下面幾點:
1、基于角色的權限控制,一個用戶可能被授予多個角色;
2、用戶對同一條記錄的不同操作(如查看和修改)需要分別授權;
3、記錄之間可能存在父子關系,子記錄的權限自動從父記錄繼承,不需要明確授權。

所以,可以看出這個權限模型存在四個基本要素:用戶、角色、操作、記錄。
用戶:實際進行操作的單位。
角色:用戶進行操作時的身份。
操作:用戶執(zhí)行的與記錄有關的動作。
記錄:被操作的對象。

因為鑒權只針對角色而非用戶,所以一個鑒權應該可以描述為:判斷某角色是否對某記錄擁有某操作的權限。

可以看出,每個權限都是一個“角色—操作—記錄”的三要素關聯(lián)關系。如果我們要設計一張表保存所有授權,那么這張表至少應該包含這三個字段。

那么問題來了:隨著記錄的增加,這張表的記錄數(shù)將呈級數(shù)增長,特別是記錄之間存在父子關系的情況下,為父記錄授權就意味著同樣要為所有子記錄授權,刪除一條授權也會造成大量的查詢和更新,這張表的維護將成為噩夢,這樣的表設計沒有實用性。

造成這種情況的根本原因是,記錄是經常變化的,而鑒權規(guī)則很少改變,二者之間存在脫節(jié)。

在這里我們不得不重新思考授權的本質:授權本來就不是針對某條具體的記錄的。以博客系統(tǒng)為例,“作者有權刪除其創(chuàng)建的文章”這個規(guī)則中,角色是“作者”,操作是“刪除”,而記錄呢?“作者創(chuàng)建的文章”并不是一條具體的記錄。所以說,授權是對規(guī)則的描述,它針對的不是具體的記錄,而是更抽象的東西。

這種更抽象的東西,我們暫把它叫做“資源”。那么鑒權的三要素,應該稱作“角色—操作—資源”。資源是對記錄的抽象,就如角色是對用戶的抽象一樣。這樣,一條權限就變成了完全抽象的:它既不針對具體的某個用戶,也不針對具體的某條記錄,它完全是對規(guī)則的描述。當一個用戶對一條記錄的操作需要鑒權時,需要進行映射,將用戶映射到角色,將記錄映射到資源,然后再搜索是否尋在允許的授權。

因為“用戶—角色”是多對多的關系,“記錄—資源”也是多對多的關系(比如一篇博客文章可能是“我的文章”,也可能是“別人的文章”),所以“用戶—操作—記錄”先要被映射到“角色[]—操作—資源[]”([]表示有多個),然后再從匹配關系的組合中搜索是否存在允許的授權。如果存在則表示鑒權通過,否則鑒權不通過。

至此為止,權限表設計已經從“角色—操作—記錄”改為“角色—操作—資源”,它符合“授權的本質”,所以不會有大量的級聯(lián)查詢和更新。但它仍然存在兩個問題:1)因為存在角色與資源的多對多組合,所以每次鑒權需要進行大量的判斷。2)我們沒有辦法實現(xiàn)從記錄到資源的映射,因為兩者并沒有直接關聯(lián)。為什么這么說呢?以博客系統(tǒng)為例,一篇文章在作者面前可以被映射為“我的文章”,在管理員面前可以被映射為“普通文章”,也許還會存在其他的映射,這是無法確定的。我們需要仔細考察這里面的關系。

經過仔細考察,我發(fā)現(xiàn)這兩個問題其實是有關聯(lián)的。我們需要重新審視資源的概念:資源不是獨立存在的,而是與角色關聯(lián)的,不同的角色需要面對不同的資源。比如博客系統(tǒng)中,管理員面對用戶、角色等資源,而普通用戶則面對文章、博客等資源。所以記錄到資源的映射與角色有著密切關系,比如一篇文章在博客作者面前要么是“我的文章”,要么是“別人的文章”,而在管理員面前要么是“普通文章”,要么是“其他文章”。這是很合理的:一條記錄在不同的角色面前以不同的角度呈現(xiàn)。

所以通過角色,我們可以將“用戶—操作—記錄”映射到“角色—操作—資源”的步驟改為:首先完成用戶到角色的映射,然后對每種角色,列出記錄到資源的映射,然后搜索是否存在允許的授權。這樣能夠明顯減少判斷的次數(shù)。

我們還要考慮記錄的繼承關系。這個邏輯不復雜:當一條記錄搜索不到授權時,還要獲取其父記錄并搜索父記錄的授權。直到所有的父記錄都找不到授權,才能返回授權不通過。

然后我們要考慮同步:對同一個“角色(不是用戶,因為這樣同步范圍更大)—操作—記錄(不是資源,因為這樣搜索更精準)”的鑒權需要進行同步鎖,這樣可以避免重復搜索浪費資源。

最后我們要考慮如何緩存。通常為了提高鑒權效率,所有已經判斷的授權都要緩存起來可以避免重復搜索。緩存是對記錄(而非資源)授權的緩存,即緩存“用戶/角色—操作—記錄”,這樣可以避免重復搜索父記錄授權。

當授權變更時,緩存如何維護?這個問題也要考慮。當添加一條授權時,我們不需要關心,因為經過一段時間的搜索,相應的緩存記錄就會自動補充起來;當一條“角色—操作—資源”授權被刪除時,需要刪除:1)所有對應的“角色—操作—記錄”緩存;2)找到角色對應的用戶,刪除所有對應的“用戶—操作—記錄”緩存。這個過程效率當然可能不高,但考慮到刪除授權本身不會很頻繁,所以應該能夠接受。

至此為止,表設計和鑒權的邏輯過程我們都清晰了,然后是如何實現(xiàn)。鑒權過程的實現(xiàn)關鍵在于映射,特別是從記錄到資源的映射。這個映射是不可能光靠數(shù)據(jù)庫配置來完成的,必須要有代碼邏輯,比如博客系統(tǒng)的文章映射到“我的文章”,就需要判斷該用戶是不是文章的作者。這樣的邏輯我們可以抽象為一個接口:

/**
 * 將記錄映射到資源的接口。不同類型的記錄應該有不同的實現(xiàn)類
 
*/
public interface ResourceMapper {

    /**
     * 根據(jù)角色將記錄映射到資源
     * 
@param recordId 記錄ID
     * 
@param roleId   角色ID
     * 
@return 資源ID
     
*/
    Long getResourceId(Long recordId, Long roleId);

    /**
     * 搜索父記錄,如果不存在則返回 null
     * 
@param recordId  記錄ID
     * 
@return 父記錄ID,如果不存在則返回 null
     
*/
    Long getParent(Long recordId);
}

至于其他的部分,本文就不贅述了。我要趕緊去實現(xiàn)一個看看。

最后總結一下這個鑒權系統(tǒng)的主要部分:
1、數(shù)據(jù)庫設計:角色表(ID,名稱),鑒權表(角色,操作,資源),資源表(ID,名稱,角色【如果為空則表示對所有角色可見】)
2、鑒權的核心邏輯:映射 + 搜索
3、鑒權的外圍邏輯:緩存
4、需要用戶實現(xiàn)的邏輯:映射接口 ResourceMapper