(本文沿著思路來寫,不會(huì)一開始就給出最后結(jié)論。)
很多項(xiàng)目需要對(duì)用戶操作進(jìn)行鑒權(quán)。它們的需求可以歸納為下面幾點(diǎn):
1、基于角色的權(quán)限控制,一個(gè)用戶可能被授予多個(gè)角色;
2、用戶對(duì)同一條記錄的不同操作(如查看和修改)需要分別授權(quán);
3、記錄之間可能存在父子關(guān)系,子記錄的權(quán)限自動(dòng)從父記錄繼承,不需要明確授權(quán)。
所以,可以看出這個(gè)權(quán)限模型存在四個(gè)基本要素:用戶、角色、操作、記錄。
用戶:實(shí)際進(jìn)行操作的單位。
角色:用戶進(jìn)行操作時(shí)的身份。
操作:用戶執(zhí)行的與記錄有關(guān)的動(dòng)作。
記錄:被操作的對(duì)象。
因?yàn)殍b權(quán)只針對(duì)角色而非用戶,所以一個(gè)鑒權(quán)應(yīng)該可以描述為:判斷某角色是否對(duì)某記錄擁有某操作的權(quán)限。
可以看出,每個(gè)權(quán)限都是一個(gè)“角色—操作—記錄”的三要素關(guān)聯(lián)關(guān)系。如果我們要設(shè)計(jì)一張表保存所有授權(quán),那么這張表至少應(yīng)該包含這三個(gè)字段。
那么問題來了:隨著記錄的增加,這張表的記錄數(shù)將呈級(jí)數(shù)增長,特別是記錄之間存在父子關(guān)系的情況下,為父記錄授權(quán)就意味著同樣要為所有子記錄授權(quán),刪除一條授權(quán)也會(huì)造成大量的查詢和更新,這張表的維護(hù)將成為噩夢(mèng),這樣的表設(shè)計(jì)沒有實(shí)用性。
造成這種情況的根本原因是,記錄是經(jīng)常變化的,而鑒權(quán)規(guī)則很少改變,二者之間存在脫節(jié)。
在這里我們不得不重新思考授權(quán)的本質(zhì):授權(quán)本來就不是針對(duì)某條具體的記錄的。以博客系統(tǒng)為例,“作者有權(quán)刪除其創(chuàng)建的文章”這個(gè)規(guī)則中,角色是“作者”,操作是“刪除”,而記錄呢?“作者創(chuàng)建的文章”并不是一條具體的記錄。所以說,授權(quán)是對(duì)規(guī)則的描述,它針對(duì)的不是具體的記錄,而是更抽象的東西。
這種更抽象的東西,我們暫把它叫做“資源”。那么鑒權(quán)的三要素,應(yīng)該稱作“角色—操作—資源”。資源是對(duì)記錄的抽象,就如角色是對(duì)用戶的抽象一樣。這樣,一條權(quán)限就變成了完全抽象的:它既不針對(duì)具體的某個(gè)用戶,也不針對(duì)具體的某條記錄,它完全是對(duì)規(guī)則的描述。當(dāng)一個(gè)用戶對(duì)一條記錄的操作需要鑒權(quán)時(shí),需要進(jìn)行映射,將用戶映射到角色,將記錄映射到資源,然后再搜索是否尋在允許的授權(quán)。
因?yàn)?#8220;用戶—角色”是多對(duì)多的關(guān)系,“記錄—資源”也是多對(duì)多的關(guān)系(比如一篇博客文章可能是“我的文章”,也可能是“別人的文章”),所以“用戶—操作—記錄”先要被映射到“角色[]—操作—資源[]”([]表示有多個(gè)),然后再從匹配關(guān)系的組合中搜索是否存在允許的授權(quán)。如果存在則表示鑒權(quán)通過,否則鑒權(quán)不通過。
至此為止,權(quán)限表設(shè)計(jì)已經(jīng)從“角色—操作—記錄”改為“角色—操作—資源”,它符合“授權(quán)的本質(zhì)”,所以不會(huì)有大量的級(jí)聯(lián)查詢和更新。但它仍然存在兩個(gè)問題:1)因?yàn)榇嬖诮巧c資源的多對(duì)多組合,所以每次鑒權(quán)需要進(jìn)行大量的判斷。2)我們沒有辦法實(shí)現(xiàn)從記錄到資源的映射,因?yàn)閮烧卟]有直接關(guān)聯(lián)。為什么這么說呢?以博客系統(tǒng)為例,一篇文章在作者面前可以被映射為“我的文章”,在管理員面前可以被映射為“普通文章”,也許還會(huì)存在其他的映射,這是無法確定的。我們需要仔細(xì)考察這里面的關(guān)系。
經(jīng)過仔細(xì)考察,我發(fā)現(xiàn)這兩個(gè)問題其實(shí)是有關(guān)聯(lián)的。我們需要重新審視資源的概念:資源不是獨(dú)立存在的,而是與角色關(guān)聯(lián)的,不同的角色需要面對(duì)不同的資源。比如博客系統(tǒng)中,管理員面對(duì)用戶、角色等資源,而普通用戶則面對(duì)文章、博客等資源。所以記錄到資源的映射與角色有著密切關(guān)系,比如一篇文章在博客作者面前要么是“我的文章”,要么是“別人的文章”,而在管理員面前要么是“普通文章”,要么是“其他文章”。這是很合理的:一條記錄在不同的角色面前以不同的角度呈現(xiàn)。
所以通過角色,我們可以將“用戶—操作—記錄”映射到“角色—操作—資源”的步驟改為:首先完成用戶到角色的映射,然后對(duì)每種角色,列出記錄到資源的映射,然后搜索是否存在允許的授權(quán)。這樣能夠明顯減少判斷的次數(shù)。
我們還要考慮記錄的繼承關(guān)系。這個(gè)邏輯不復(fù)雜:當(dāng)一條記錄搜索不到授權(quán)時(shí),還要獲取其父記錄并搜索父記錄的授權(quán)。直到所有的父記錄都找不到授權(quán),才能返回授權(quán)不通過。
然后我們要考慮同步:對(duì)同一個(gè)“角色(不是用戶,因?yàn)檫@樣同步范圍更大)—操作—記錄(不是資源,因?yàn)檫@樣搜索更精準(zhǔn))”的鑒權(quán)需要進(jìn)行同步鎖,這樣可以避免重復(fù)搜索浪費(fèi)資源。
最后我們要考慮如何緩存。通常為了提高鑒權(quán)效率,所有已經(jīng)判斷的授權(quán)都要緩存起來可以避免重復(fù)搜索。緩存是對(duì)記錄(而非資源)授權(quán)的緩存,即緩存“用戶/角色—操作—記錄”,這樣可以避免重復(fù)搜索父記錄授權(quán)。
當(dāng)授權(quán)變更時(shí),緩存如何維護(hù)?這個(gè)問題也要考慮。當(dāng)添加一條授權(quán)時(shí),我們不需要關(guān)心,因?yàn)榻?jīng)過一段時(shí)間的搜索,相應(yīng)的緩存記錄就會(huì)自動(dòng)補(bǔ)充起來;當(dāng)一條“角色—操作—資源”授權(quán)被刪除時(shí),需要?jiǎng)h除:1)所有對(duì)應(yīng)的“角色—操作—記錄”緩存;2)找到角色對(duì)應(yīng)的用戶,刪除所有對(duì)應(yīng)的“用戶—操作—記錄”緩存。這個(gè)過程效率當(dāng)然可能不高,但考慮到刪除授權(quán)本身不會(huì)很頻繁,所以應(yīng)該能夠接受。
至此為止,表設(shè)計(jì)和鑒權(quán)的邏輯過程我們都清晰了,然后是如何實(shí)現(xiàn)。鑒權(quán)過程的實(shí)現(xiàn)關(guān)鍵在于映射,特別是從記錄到資源的映射。這個(gè)映射是不可能光靠數(shù)據(jù)庫配置來完成的,必須要有代碼邏輯,比如博客系統(tǒng)的文章映射到“我的文章”,就需要判斷該用戶是不是文章的作者。這樣的邏輯我們可以抽象為一個(gè)接口:
至于其他的部分,本文就不贅述了。我要趕緊去實(shí)現(xiàn)一個(gè)看看。
最后總結(jié)一下這個(gè)鑒權(quán)系統(tǒng)的主要部分:
1、數(shù)據(jù)庫設(shè)計(jì):角色表(ID,名稱),鑒權(quán)表(角色,操作,資源),資源表(ID,名稱,角色【如果為空則表示對(duì)所有角色可見】)
2、鑒權(quán)的核心邏輯:映射 + 搜索
3、鑒權(quán)的外圍邏輯:緩存
4、需要用戶實(shí)現(xiàn)的邏輯:映射接口 ResourceMapper
很多項(xiàng)目需要對(duì)用戶操作進(jìn)行鑒權(quán)。它們的需求可以歸納為下面幾點(diǎn):
1、基于角色的權(quán)限控制,一個(gè)用戶可能被授予多個(gè)角色;
2、用戶對(duì)同一條記錄的不同操作(如查看和修改)需要分別授權(quán);
3、記錄之間可能存在父子關(guān)系,子記錄的權(quán)限自動(dòng)從父記錄繼承,不需要明確授權(quán)。
所以,可以看出這個(gè)權(quán)限模型存在四個(gè)基本要素:用戶、角色、操作、記錄。
用戶:實(shí)際進(jìn)行操作的單位。
角色:用戶進(jìn)行操作時(shí)的身份。
操作:用戶執(zhí)行的與記錄有關(guān)的動(dòng)作。
記錄:被操作的對(duì)象。
因?yàn)殍b權(quán)只針對(duì)角色而非用戶,所以一個(gè)鑒權(quán)應(yīng)該可以描述為:判斷某角色是否對(duì)某記錄擁有某操作的權(quán)限。
可以看出,每個(gè)權(quán)限都是一個(gè)“角色—操作—記錄”的三要素關(guān)聯(lián)關(guān)系。如果我們要設(shè)計(jì)一張表保存所有授權(quán),那么這張表至少應(yīng)該包含這三個(gè)字段。
那么問題來了:隨著記錄的增加,這張表的記錄數(shù)將呈級(jí)數(shù)增長,特別是記錄之間存在父子關(guān)系的情況下,為父記錄授權(quán)就意味著同樣要為所有子記錄授權(quán),刪除一條授權(quán)也會(huì)造成大量的查詢和更新,這張表的維護(hù)將成為噩夢(mèng),這樣的表設(shè)計(jì)沒有實(shí)用性。
造成這種情況的根本原因是,記錄是經(jīng)常變化的,而鑒權(quán)規(guī)則很少改變,二者之間存在脫節(jié)。
在這里我們不得不重新思考授權(quán)的本質(zhì):授權(quán)本來就不是針對(duì)某條具體的記錄的。以博客系統(tǒng)為例,“作者有權(quán)刪除其創(chuàng)建的文章”這個(gè)規(guī)則中,角色是“作者”,操作是“刪除”,而記錄呢?“作者創(chuàng)建的文章”并不是一條具體的記錄。所以說,授權(quán)是對(duì)規(guī)則的描述,它針對(duì)的不是具體的記錄,而是更抽象的東西。
這種更抽象的東西,我們暫把它叫做“資源”。那么鑒權(quán)的三要素,應(yīng)該稱作“角色—操作—資源”。資源是對(duì)記錄的抽象,就如角色是對(duì)用戶的抽象一樣。這樣,一條權(quán)限就變成了完全抽象的:它既不針對(duì)具體的某個(gè)用戶,也不針對(duì)具體的某條記錄,它完全是對(duì)規(guī)則的描述。當(dāng)一個(gè)用戶對(duì)一條記錄的操作需要鑒權(quán)時(shí),需要進(jìn)行映射,將用戶映射到角色,將記錄映射到資源,然后再搜索是否尋在允許的授權(quán)。
因?yàn)?#8220;用戶—角色”是多對(duì)多的關(guān)系,“記錄—資源”也是多對(duì)多的關(guān)系(比如一篇博客文章可能是“我的文章”,也可能是“別人的文章”),所以“用戶—操作—記錄”先要被映射到“角色[]—操作—資源[]”([]表示有多個(gè)),然后再從匹配關(guān)系的組合中搜索是否存在允許的授權(quán)。如果存在則表示鑒權(quán)通過,否則鑒權(quán)不通過。
至此為止,權(quán)限表設(shè)計(jì)已經(jīng)從“角色—操作—記錄”改為“角色—操作—資源”,它符合“授權(quán)的本質(zhì)”,所以不會(huì)有大量的級(jí)聯(lián)查詢和更新。但它仍然存在兩個(gè)問題:1)因?yàn)榇嬖诮巧c資源的多對(duì)多組合,所以每次鑒權(quán)需要進(jìn)行大量的判斷。2)我們沒有辦法實(shí)現(xiàn)從記錄到資源的映射,因?yàn)閮烧卟]有直接關(guān)聯(lián)。為什么這么說呢?以博客系統(tǒng)為例,一篇文章在作者面前可以被映射為“我的文章”,在管理員面前可以被映射為“普通文章”,也許還會(huì)存在其他的映射,這是無法確定的。我們需要仔細(xì)考察這里面的關(guān)系。
經(jīng)過仔細(xì)考察,我發(fā)現(xiàn)這兩個(gè)問題其實(shí)是有關(guān)聯(lián)的。我們需要重新審視資源的概念:資源不是獨(dú)立存在的,而是與角色關(guān)聯(lián)的,不同的角色需要面對(duì)不同的資源。比如博客系統(tǒng)中,管理員面對(duì)用戶、角色等資源,而普通用戶則面對(duì)文章、博客等資源。所以記錄到資源的映射與角色有著密切關(guān)系,比如一篇文章在博客作者面前要么是“我的文章”,要么是“別人的文章”,而在管理員面前要么是“普通文章”,要么是“其他文章”。這是很合理的:一條記錄在不同的角色面前以不同的角度呈現(xiàn)。
所以通過角色,我們可以將“用戶—操作—記錄”映射到“角色—操作—資源”的步驟改為:首先完成用戶到角色的映射,然后對(duì)每種角色,列出記錄到資源的映射,然后搜索是否存在允許的授權(quán)。這樣能夠明顯減少判斷的次數(shù)。
我們還要考慮記錄的繼承關(guān)系。這個(gè)邏輯不復(fù)雜:當(dāng)一條記錄搜索不到授權(quán)時(shí),還要獲取其父記錄并搜索父記錄的授權(quán)。直到所有的父記錄都找不到授權(quán),才能返回授權(quán)不通過。
然后我們要考慮同步:對(duì)同一個(gè)“角色(不是用戶,因?yàn)檫@樣同步范圍更大)—操作—記錄(不是資源,因?yàn)檫@樣搜索更精準(zhǔn))”的鑒權(quán)需要進(jìn)行同步鎖,這樣可以避免重復(fù)搜索浪費(fèi)資源。
最后我們要考慮如何緩存。通常為了提高鑒權(quán)效率,所有已經(jīng)判斷的授權(quán)都要緩存起來可以避免重復(fù)搜索。緩存是對(duì)記錄(而非資源)授權(quán)的緩存,即緩存“用戶/角色—操作—記錄”,這樣可以避免重復(fù)搜索父記錄授權(quán)。
當(dāng)授權(quán)變更時(shí),緩存如何維護(hù)?這個(gè)問題也要考慮。當(dāng)添加一條授權(quán)時(shí),我們不需要關(guān)心,因?yàn)榻?jīng)過一段時(shí)間的搜索,相應(yīng)的緩存記錄就會(huì)自動(dòng)補(bǔ)充起來;當(dāng)一條“角色—操作—資源”授權(quán)被刪除時(shí),需要?jiǎng)h除:1)所有對(duì)應(yīng)的“角色—操作—記錄”緩存;2)找到角色對(duì)應(yīng)的用戶,刪除所有對(duì)應(yīng)的“用戶—操作—記錄”緩存。這個(gè)過程效率當(dāng)然可能不高,但考慮到刪除授權(quán)本身不會(huì)很頻繁,所以應(yīng)該能夠接受。
至此為止,表設(shè)計(jì)和鑒權(quán)的邏輯過程我們都清晰了,然后是如何實(shí)現(xiàn)。鑒權(quán)過程的實(shí)現(xiàn)關(guān)鍵在于映射,特別是從記錄到資源的映射。這個(gè)映射是不可能光靠數(shù)據(jù)庫配置來完成的,必須要有代碼邏輯,比如博客系統(tǒng)的文章映射到“我的文章”,就需要判斷該用戶是不是文章的作者。這樣的邏輯我們可以抽象為一個(gè)接口:
/**
* 將記錄映射到資源的接口。不同類型的記錄應(yīng)該有不同的實(shí)現(xiàn)類
*/
public interface ResourceMapper {
/**
* 根據(jù)角色將記錄映射到資源
* @param recordId 記錄ID
* @param roleId 角色I(xiàn)D
* @return 資源ID
*/
Long getResourceId(Long recordId, Long roleId);
/**
* 搜索父記錄,如果不存在則返回 null
* @param recordId 記錄ID
* @return 父記錄ID,如果不存在則返回 null
*/
Long getParent(Long recordId);
}
* 將記錄映射到資源的接口。不同類型的記錄應(yīng)該有不同的實(shí)現(xiàn)類
*/
public interface ResourceMapper {
/**
* 根據(jù)角色將記錄映射到資源
* @param recordId 記錄ID
* @param roleId 角色I(xiàn)D
* @return 資源ID
*/
Long getResourceId(Long recordId, Long roleId);
/**
* 搜索父記錄,如果不存在則返回 null
* @param recordId 記錄ID
* @return 父記錄ID,如果不存在則返回 null
*/
Long getParent(Long recordId);
}
至于其他的部分,本文就不贅述了。我要趕緊去實(shí)現(xiàn)一個(gè)看看。
最后總結(jié)一下這個(gè)鑒權(quán)系統(tǒng)的主要部分:
1、數(shù)據(jù)庫設(shè)計(jì):角色表(ID,名稱),鑒權(quán)表(角色,操作,資源),資源表(ID,名稱,角色【如果為空則表示對(duì)所有角色可見】)
2、鑒權(quán)的核心邏輯:映射 + 搜索
3、鑒權(quán)的外圍邏輯:緩存
4、需要用戶實(shí)現(xiàn)的邏輯:映射接口 ResourceMapper