終于又要寫技術(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,B和B,A是等價(jià)的,好像又有哪里怪怪的……
好吧,至少以后授權(quán)名稱里不要出現(xiàn)冒號(hào)、逗號(hào)和星號(hào)就是了,也要避免兩個(gè)授權(quán)間存在包含關(guān)系。
Shiro的授權(quán)判定著實(shí)有一些令人迷惑啊……