Shiro授权的谜之判定方法

2019-12-11  本文已影响0人  康加罗

终于又要写技术相关了,这次搞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的授权判定着实有一些令人迷惑啊……

上一篇下一篇

猜你喜欢

热点阅读