Shiro授權(quán)的謎之判定方法

終于又要寫技術(shù)相關(guān)了,這次搞Shiro。(其實(shí)是被Shiro搞了……)

0、起因

系統(tǒng)里用了shiro對(duì)用戶的訪問權(quán)限做了限制,主要分為兩類:菜單,接口。兩類權(quán)限都是以字符串的方式保存英文名稱,其中接口權(quán)限的組成方式為

menuName:methodName

在接口上增加@RequiresPermissions注解,根據(jù)用戶登錄時(shí)獲取的授權(quán)列表,交由shiro判斷當(dāng)前接口是否獲得授權(quán),允許用戶訪問。

1、問題

因?yàn)橄葴y(cè)試頁面訪問,所以菜單授權(quán)提前添加了,但是接口授權(quán)是后續(xù)增加的,增加之后出現(xiàn)了一個(gè)問題——在沒有授權(quán)的情況下,有的接口被限制了訪問,提示接口未授權(quán);有的接口沒有被限制,能夠正常返回?cái)?shù)據(jù)。

測(cè)試了8個(gè)菜單頁面,7個(gè)的接口都沒有限制,只有1個(gè)成功了,這就有點(diǎn)神奇了吧。

有成功的接口,說明@RequiresPermissions生效了,但是似乎其它的接口被判定為已授權(quán)。

首先我想通過本地代碼測(cè)試一下,在成功繞過限制的接口里看看當(dāng)前用戶的授權(quán)列表。由于獲取授權(quán)列表的方法是本地重寫的,我從源碼里摘出了獲取授權(quán)信息的方法

RealmSecurityManager rsm = (RealmSecurityManager) SecurityUtils.getSecurityManager();
AuthorizingRealm shiroRealm = (AuthorizingRealm) rsm.getRealms().iterator().next();
Cache<Object, AuthorizationInfo> authorizetionInfo = shiroRealm.getAuthorizationCache();

此時(shí)authorizetionInfo以key-value的形式存儲(chǔ)了用戶及授權(quán)列表,通過authorizetionInfo.keys()方法也確認(rèn)找到了當(dāng)前用戶,但是通過get方法獲取時(shí)返回值為null。

為什么獲取不到呢?因?yàn)闆]有通過本地代碼登錄嗎?這個(gè)疑問沒能解決,我決定換個(gè)方法。

考慮授權(quán)列表的頻繁對(duì)比,我們將授權(quán)列表寫到了redis中,那我直接查看redis中的數(shù)據(jù)不就行了嗎?

打開redis可視化工具,找到用戶權(quán)限存儲(chǔ),好的,16進(jìn)制漢字存儲(chǔ)……

還是用原始樸素的命令行工具吧。

./redis-cli --raw

中文顯示。對(duì)比了redis中的授權(quán)數(shù)據(jù)和實(shí)際期望的授權(quán)列表,確認(rèn)一致。

那么還有一種可能就是,雖然授權(quán)列表里只有菜單,但是接口依然被shiro認(rèn)為是通過驗(yàn)證的。那么shiro的驗(yàn)證方法是怎樣的呢?

2、測(cè)試

簡(jiǎn)化一下,目前授權(quán)了兩個(gè)菜單,分別為

MenuA
MenuB

同時(shí)有兩個(gè)接口添加了限制但沒有授權(quán),分別為

MenuA:methodOne
MenuB:methodOne

(對(duì),用的是同名接口)

目前MenuA:methodOne限制失敗,可以訪問;MenuB:methodOne限制成功,訪問失敗。

那么我們用最簡(jiǎn)單粗暴的方式,直接看判斷結(jié)果

Subject subject = SecurityUtils.getSubject();
Boolean permissionA = subject.isPermitted("MenuA:methodOne");
Boolean permissionB = subject.isPermitted("MenuBs:methodOne");

第一項(xiàng)結(jié)果為TRUE,通過驗(yàn)證;第二項(xiàng)結(jié)果為FALSE,未通過驗(yàn)證,和實(shí)際訪問情況一致。

等等,為什么出現(xiàn)了“MenuBs:methodOne”?

重新看了一下授權(quán)數(shù)據(jù),MenuB下的接口在錄入過程中多添加了一個(gè)字母s,而MenuA下的方法沒有這個(gè)情況。再查看其它6個(gè)限制失敗的頁面,與MenuA一樣。所以這就是MenuB下接口限制成功的原因?

3、源碼

原因找到了,但是背后的原理呢?

在網(wǎng)上找到了一篇博客Shiro @RequiresPermissions是如何運(yùn)轉(zhuǎn)的?,展示了shiro的判斷邏輯,這下看來要看看shiro源碼了。

3-1、獲取

首先在org.apache.shiro.realm.AuthorizingRealm中我們看到了方法名getAuthorizationInfo,非常直白地告訴我們,我是在這獲取授權(quán)信息的。

protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {

? ? ? ? if (principals == null) {

? ? ? ? ? ? return null;

? ? ? ? }

? ? ? ? AuthorizationInfo info = null;

? ? ? ? if (log.isTraceEnabled()) {

? ? ? ? ? ? log.trace("Retrieving AuthorizationInfo for principals [" + principals + "]");

? ? ? ? }

? ? ? ? Cache<Object, AuthorizationInfo> cache = getAvailableAuthorizationCache();

? ? ? ? if (cache != null) {

? ? ? ? ? ? if (log.isTraceEnabled()) {

? ? ? ? ? ? ? ? log.trace("Attempting to retrieve the AuthorizationInfo from cache.");

? ? ? ? ? ? }

? ? ? ? ? ? Object key = getAuthorizationCacheKey(principals);

? ? ? ? ? ? info = cache.get(key);

? ? ? ? ? ? if (log.isTraceEnabled()) {

? ? ? ? ? ? ? ? if (info == null) {

? ? ? ? ? ? ? ? ? ? log.trace("No AuthorizationInfo found in cache for principals [" + principals + "]");

? ? ? ? ? ? ? ? } else {

? ? ? ? ? ? ? ? ? ? log.trace("AuthorizationInfo found in cache for principals [" + principals + "]");

? ? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? ? ? }

? ? ? ? if (info == null) {

? ? ? ? ? ? // Call template method if the info was not found in a cache

? ? ? ? ? ? info = doGetAuthorizationInfo(principals);

? ? ? ? ? ? // If the info is not null and the cache has been created, then cache the authorization info.

? ? ? ? ? ? if (info != null && cache != null) {

? ? ? ? ? ? ? ? if (log.isTraceEnabled()) {

? ? ? ? ? ? ? ? ? ? log.trace("Caching authorization info for principals: [" + principals + "].");

? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? Object key = getAuthorizationCacheKey(principals);

? ? ? ? ? ? ? ? cache.put(key, info);

? ? ? ? ? ? }

? ? ? ? }

? ? ? ? return info;

? ? }

大概就是先去cache里找緩存數(shù)據(jù),如果沒有找到,那么就要通過doGetAuthorizationInfo(principals);獲取了。接下來——

protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollectionprincipals);

嗯,抽象方法,也就是說我們要自己實(shí)現(xiàn)。想到我們本地重寫的授權(quán)列表獲取的源碼……一切都水落石出了?。ú皇牵?/p>

3-2 判定

再往下看會(huì)發(fā)現(xiàn)一系列的isPermitted方法,挨個(gè)看去找到了判定的最終歸宿(也就是上邊博文里截出來的那段)

protected boolean isPermitted(Permission permission, AuthorizationInfo info) {

? ? ? ? Collection<Permission> perms = getPermissions(info);

? ? ? ? if (perms != null && !perms.isEmpty()) {

? ? ? ? ? ? for (Permission perm : perms) {

? ? ? ? ? ? ? ? if (perm.implies(permission)) {

? ? ? ? ? ? ? ? ? ? return true;

? ? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? ? ? }

? ? ? ? return false;

? ? }

其中的implies方法就是重點(diǎn)了,那么它在哪兒呢?

根據(jù)引用我們找到了org.apache.shiro.authz.permission,好的是個(gè)接口類……

不如用implements Permission作為關(guān)鍵字搜索一下源碼吧!

首先我們找到了org.apache.shiro.authz.permission.AllPermission;

public boolean implies(Permission p) {

???? return true;

}

行,這不是我們要找的。

然后就是org.apache.shiro.authz.permission.WildcardPermission了。是它!

public boolean implies(Permission p) {

? ? ? ? // By default only supports comparisons with other WildcardPermissions

? ? ? ? if (!(p instanceof WildcardPermission)) {

? ? ? ? ? ? return false;

? ? ? ? }

? ? ? ? WildcardPermission wp = (WildcardPermission) p;

? ? ? ? List<Set<String>> otherParts = wp.getParts();

? ? ? ? int i = 0;

? ? ? ? for (Set<String> otherPart : otherParts) {

? ? ? ? ? ? // If this permission has less parts than the other permission, everything after the number of parts contained

? ? ? ? ? ? // in this permission is automatically implied, so return true

? ? ? ? ? ? if (getParts().size() - 1 < i) {

? ? ? ? ? ? ? ? return true;

? ? ? ? ? ? } else {

? ? ? ? ? ? ? ? Set<String> part = getParts().get(i);

? ? ? ? ? ? ? ? if (!part.contains(WILDCARD_TOKEN) && !part.containsAll(otherPart)) {

? ? ? ? ? ? ? ? ? ? return false;

? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? i++;

? ? ? ? ? ? }

? ? ? ? }

? ? ? ? // If this permission has more parts than the other parts, only imply it if all of the other parts are wildcards

? ? ? ? for (; i < getParts().size(); i++) {

? ? ? ? ? ? Set<String> part = getParts().get(i);

? ? ? ? ? ? if (!part.contains(WILDCARD_TOKEN)) {

? ? ? ? ? ? ? ? return false;

? ? ? ? ? ? }

? ? ? ? }

? ? ? ? return true;? ?

}

沒錯(cuò)!是博文里提到的!

那么這段的邏輯是怎樣的呢?

首先我必須要說,this other這種起名方式是什么鬼!

簡(jiǎn)單來說,Permission會(huì)被當(dāng)做字符串進(jìn)行分割,一個(gè)Permission會(huì)分割為兩級(jí),第一級(jí)使用PART_DIVIDER_TOKEN(英文半角冒號(hào):)分割,分割后的部分會(huì)被存入一個(gè)List<String>中;然后對(duì)List進(jìn)行遍歷,對(duì)每一部分進(jìn)行二級(jí)分割,使用SUBPART_DIVIDER_TOKEN(英文半角逗號(hào),),分割結(jié)果放在Set<String>中,也就是最后我們將得到一個(gè)List<Set<String>>作為判定依據(jù)。

假設(shè)我們需要對(duì)比授權(quán)列表中的授權(quán)PermissionAuthed和當(dāng)前訪問的接口授權(quán)PermissionCurrent,那么首先將兩個(gè)permission進(jìn)行上述分割,然后對(duì)PermissionCurrent的list進(jìn)行遍歷,一一對(duì)比兩個(gè)list中的集合,如果PermissionAuthed的set為PermissionCurrent的set的超集,或者PermissionAuthed的set中包含通配符WILDCARD_TOKEN(英文半角星號(hào)*),那么對(duì)比繼續(xù);否則對(duì)比失敗,授權(quán)沒有通過驗(yàn)證。

如果在對(duì)比繼續(xù)的情況下,二者的List長(zhǎng)度不相等,那么——

1、PermissionCurrent長(zhǎng)度較長(zhǎng),則認(rèn)為包含了PermissionAuthed的授權(quán),對(duì)比成功,授權(quán)通過驗(yàn)證;

2、PermissionAuthed長(zhǎng)度較長(zhǎng),那么遍歷PermissionAuthed的剩余部分,如果剩余部分中每一個(gè)Set的都包含通配符WILDCARD_TOKEN(英文半角星號(hào)*),那么對(duì)比成功,授權(quán)通過驗(yàn)證;否則對(duì)比失敗,授權(quán)沒有通過驗(yàn)證。

于是我們找到了MenuA菜單下方法通過授權(quán)驗(yàn)證的原因,對(duì)于上述情況

PermissionAuthed = List { Set [ menuA ] }
PermissionCurrent = List { Set [ menuA ], Set [ methodOne ] }

顯然PermissionCurrent較長(zhǎng)且通過了PermissionAuthed中的所有對(duì)比。

我們得出結(jié)論,如果PermissionA是PermissionB的子串,那么當(dāng)對(duì)PermissionA授權(quán)后,PermissionB也能通過shiro的授權(quán)驗(yàn)證。

有一種合理但是又哪里怪怪的感覺……

另外就是我發(fā)現(xiàn)在對(duì)比時(shí),最終對(duì)比的是Set,也就是說第二級(jí)分割后字符串就是無序的了,此時(shí)A,BB,A是等價(jià)的,好像又有哪里怪怪的……

好吧,至少以后授權(quán)名稱里不要出現(xiàn)冒號(hào)、逗號(hào)和星號(hào)就是了,也要避免兩個(gè)授權(quán)間存在包含關(guān)系。

Shiro的授權(quán)判定著實(shí)有一些令人迷惑啊……

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容