Shiro授权的谜之判定方法

终于又要写技术相关了,这次搞Shiro。(其实是被Shiro搞了……)

0、起因

系统里用了shiro对用户的访问权限做了限制,主要分为两类:菜单,接口。两类权限都是以字符串的方式保存英文名称,其中接口权限的组成方式为

menuName:methodName

在接口上增加@RequiresPermissions注解,根据用户登录时获取的授权列表,交由shiro判断当前接口是否获得授权,允许用户访问。

1、问题

因为先测试页面访问,所以菜单授权提前添加了,但是接口授权是后续增加的,增加之后出现了一个问题——在没有授权的情况下,有的接口被限制了访问,提示接口未授权;有的接口没有被限制,能够正常返回数据。

测试了8个菜单页面,7个的接口都没有限制,只有1个成功了,这就有点神奇了吧。

有成功的接口,说明@RequiresPermissions生效了,但是似乎其它的接口被判定为已授权。

首先我想通过本地代码测试一下,在成功绕过限制的接口里看看当前用户的授权列表。由于获取授权列表的方法是本地重写的,我从源码里摘出了获取授权信息的方法

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

此时authorizetionInfo以key-value的形式存储了用户及授权列表,通过authorizetionInfo.keys()方法也确认找到了当前用户,但是通过get方法获取时返回值为null。

为什么获取不到呢?因为没有通过本地代码登录吗?这个疑问没能解决,我决定换个方法。

考虑授权列表的频繁对比,我们将授权列表写到了redis中,那我直接查看redis中的数据不就行了吗?

打开redis可视化工具,找到用户权限存储,好的,16进制汉字存储……

还是用原始朴素的命令行工具吧。

./redis-cli --raw

中文显示。对比了redis中的授权数据和实际期望的授权列表,确认一致。

那么还有一种可能就是,虽然授权列表里只有菜单,但是接口依然被shiro认为是通过验证的。那么shiro的验证方法是怎样的呢?

2、测试

简化一下,目前授权了两个菜单,分别为

MenuA
MenuB

同时有两个接口添加了限制但没有授权,分别为

MenuA:methodOne
MenuB:methodOne

(对,用的是同名接口)

目前MenuA:methodOne限制失败,可以访问;MenuB:methodOne限制成功,访问失败。

那么我们用最简单粗暴的方式,直接看判断结果

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

第一项结果为TRUE,通过验证;第二项结果为FALSE,未通过验证,和实际访问情况一致。

等等,为什么出现了“MenuBs:methodOne”?

重新看了一下授权数据,MenuB下的接口在录入过程中多添加了一个字母s,而MenuA下的方法没有这个情况。再查看其它6个限制失败的页面,与MenuA一样。所以这就是MenuB下接口限制成功的原因?

3、源码

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

在网上找到了一篇博客Shiro @RequiresPermissions是如何运转的?,展示了shiro的判断逻辑,这下看来要看看shiro源码了。

3-1、获取

首先在org.apache.shiro.realm.AuthorizingRealm中我们看到了方法名getAuthorizationInfo,非常直白地告诉我们,我是在这获取授权信息的。

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里找缓存数据,如果没有找到,那么就要通过doGetAuthorizationInfo(principals);获取了。接下来——

protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollectionprincipals);

嗯,抽象方法,也就是说我们要自己实现。想到我们本地重写的授权列表获取的源码……一切都水落石出了!(不是)

3-2 判定

再往下看会发现一系列的isPermitted方法,挨个看去找到了判定的最终归宿(也就是上边博文里截出来的那段)

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方法就是重点了,那么它在哪儿呢?

根据引用我们找到了org.apache.shiro.authz.permission,好的是个接口类……

不如用implements Permission作为关键字搜索一下源码吧!

首先我们找到了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;   

}

没错!是博文里提到的!

那么这段的逻辑是怎样的呢?

首先我必须要说,this other这种起名方式是什么鬼!

简单来说,Permission会被当做字符串进行分割,一个Permission会分割为两级,第一级使用PART_DIVIDER_TOKEN(英文半角冒号:)分割,分割后的部分会被存入一个List<String>中;然后对List进行遍历,对每一部分进行二级分割,使用SUBPART_DIVIDER_TOKEN(英文半角逗号,),分割结果放在Set<String>中,也就是最后我们将得到一个List<Set<String>>作为判定依据。

假设我们需要对比授权列表中的授权PermissionAuthed和当前访问的接口授权PermissionCurrent,那么首先将两个permission进行上述分割,然后对PermissionCurrent的list进行遍历,一一对比两个list中的集合,如果PermissionAuthed的set为PermissionCurrent的set的超集,或者PermissionAuthed的set中包含通配符WILDCARD_TOKEN(英文半角星号*),那么对比继续;否则对比失败,授权没有通过验证。

如果在对比继续的情况下,二者的List长度不相等,那么——

1、PermissionCurrent长度较长,则认为包含了PermissionAuthed的授权,对比成功,授权通过验证;

2、PermissionAuthed长度较长,那么遍历PermissionAuthed的剩余部分,如果剩余部分中每一个Set的都包含通配符WILDCARD_TOKEN(英文半角星号*),那么对比成功,授权通过验证;否则对比失败,授权没有通过验证。

于是我们找到了MenuA菜单下方法通过授权验证的原因,对于上述情况

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

显然PermissionCurrent较长且通过了PermissionAuthed中的所有对比。

我们得出结论,如果PermissionA是PermissionB的子串,那么当对PermissionA授权后,PermissionB也能通过shiro的授权验证。

有一种合理但是又哪里怪怪的感觉……

另外就是我发现在对比时,最终对比的是Set,也就是说第二级分割后字符串就是无序的了,此时A,BB,A是等价的,好像又有哪里怪怪的……

好吧,至少以后授权名称里不要出现冒号、逗号和星号就是了,也要避免两个授权间存在包含关系。

Shiro的授权判定着实有一些令人迷惑啊……

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容