美文网首页
Shiro授权的谜之判定方法

Shiro授权的谜之判定方法

作者: 康加罗 | 来源:发表于2019-12-11 20:21 被阅读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的授权判定着实有一些令人迷惑啊……

    相关文章

      网友评论

          本文标题:Shiro授权的谜之判定方法

          本文链接:https://www.haomeiwen.com/subject/mmragctx.html