一看就会!一篇全搞定!权限处理专家--Shiro保姆式教学,超详细!

前言

Shiro是一个轻量级的权限处理框架,提供了身份验证,授权,加密,会话管理的功能,Shiro可以适合任何程序,从大型的Web应用到小型的命令行程序都可以使用,从整合数据库验证信息到硬编码用户信息到.ini文件一并支持。

Shiro三大对象

Shiro框架里有三个核心元素,分别是Subject(主体)SecurityManager(安全管理者)Realm(域),这三个核心元素构成了Shiro的核心功能,下面我们将逐个讲解。

Subject

在Shiro中,和程序产生交互行为的对象叫做Subject,比如说一个用户要登录一个网页,那么这个用户就是一个Subject,但是我们知道,和程序交互的并不一定是个“人”,页面可以通过爬虫访问,也可以模拟浏览器行为进行访问,所以在Shiro框架中,并不把产生交互行为的所有对象都称作“人”,而是称作一个主体–Subject,一切和用户有关的行为都是通过Subject来控制的。

SecurityManager

SecurityManager就是安全管理员,就好像一个拦路的强盗,可以在这个管理员对象里对请求进行拦截,设置允许的请求路径,设置相应的拦截路径,比如你要走上一座山,有好几条路可以走,其中有几条路是有拦路强盗的,但是如果你遇到强盗说对了暗号,那么强盗也可以放你过去(权限验证通过),但是如果你没有说对暗号,那么强盗就会把你拦下来(权限验证失败),或者带你去山寨里(重定向)

Realm

Realm是一个域,这个域的作用就是验证与授权,就好像你要拿着令牌进城门,那个守城的保安队长就要看令牌上面画的画像、写的名字是不是你,和你对不对得上,再如果你要进紫禁城去面见圣上,还得检查你有没有资格进紫禁城,这个域的作用其实就好像是一个令牌池,负责记录哪块令牌是谁,谁可以进城,负责校验令牌,查看令牌权限,也就是我们后面会提到的Authentication(验证)与Authorization(授权)。
这个Realm在Shiro框架里类似一个非常安全的操作的数据库,从数据库里拿数据,检查,验证,授权。

Authentication和Authorization

Authentication

Authentication(验证),即检测用户是不是该用户,查查看你是不是假冒别人的身份,偷偷拿了别人的令牌,这一步验证的作用就是证明自己确实是自己而不是别人

Authorization

Authorization(授权),即检测用户是不是有权限,查查你是不是够资格干这个事情,比如老师说班长可以放学早点走,你也向早点走,老师就会问你:你是班长吗?如果按照Shiro框架的做法,老师还会问一句:你是XXX吗?你是班长吗?

你够资格吗?也许不够。
-----知名哲学家 凯隐和拉亚斯特

Authentication和Authorization小结

验证和授权其实每天都发生在我们身边,比如以前坐动车得提供动车票和身份证,身份证证明你是本人,动车票证明你有资格坐动车,身份证验证,动车票授权,不能把验证和授权混为一谈。
在Shiro框架的Realm里会有两个方法,一个是doGetAuthentication,一个是doGetAuthorization,前者进行验证身份操作,后者进行授权操作,都是我们实现域对象需要重写的方法。

权限控制的三个基本要素

用户

用户就是确定一个用户是谁,比如说确定小文是小文

角色

角色代表一种权限的集合,比如说一个图书馆管理员,他可以增加书籍,删除书籍,甚至可以优先借走书籍,这都是图书馆管理员的权限,一个管理员代表着一堆权利的集合体,我们在程序中可以能会遇到非常多操作权限的地方,如果一个人既要操作图书,又要操作用户,每次都判断对应的权限实在是太过麻烦,我们不妨抽象出一个角色,每次都判断这个操作的用户是不是这个角色,如果是就放行,如果不是就拒绝,这样后期添加权限,删除权限都会比较轻松,只需要修改角色对应的权限即可。

权限

这是一个最细粒度的权限管理范围,比如说有这样一个权限管理表达式 “user:query:01”,表达的是可以对User类的01实例进行query操作,再比如说,"user:*"这个表达式代表着可以对User类的所有实例进行任意操作,很神奇吧!上面看到的两个表达式就是Shiro框架中使用的权限表达式,三个粒度的权限范围用两个 “:” 分开。

权限表达式是如何工作的呢?

权限表达式并不是直接对数据库进行控制,也就是说他并不能阻止你去操作数据库,说到底他只是一个字符串,他并不知道哪个对象是属于User类,哪个对象是User类的01实例,哪个操作是query,他只知道自己代表着一种权力就是 这个用户可以查询User类的01实例信息,它仅仅代表着一种权力,想要让权力生效,必须在操作之前加上权力的判断,也就是鉴权,举一个简单的例子:一个用户想要操作数据库修改管理员信息,在他发起请求之后,被SecurityManager给拦截了,SecurityManager会对他进行身份验证,看看你到底是不是一个用户,发现你是,那没问题,继续进行权限判断,从数据库或者ini文件中读取一个代表你这个角色的权限表达式,然后跟你要进行操作的权限表达式进行匹配,如果匹配上了,恭喜你,你可以修改管理员信息了,如果匹配不上,那么你的请求就会被拦截。
综上所述,权限表达式并不能直接去控制你能不能操作对象或者数据库,但是可以通过权限表达式的匹配来判断你有没有这个权力,决定你的请求可不可以生效,再说的通俗一点,权限表达式仅仅是一个代表权限表达式!

Shiro单机示例

Shiro可以运行在任何的程序中,为了先带领大家稍微领略一下Shiro的魅力,我选择了单机运行程序,硬编码配置文件的方法使用Shiro

解读ini配置文件

在这里插入图片描述
Shiro的ini配置文件比较简单,写起来很轻松,对于Shiro来说ini文件分为几个空间,main空间user空间,roles空间,三个不同的空间有什么用呢?
main空间一般用作全局的配置文件,比如说shiro缓存设置等等
users空间就是我们一般俗称的账号密码,看到上图里的
climbingxiaowen即为账号,nihao即为密码,中间用等号相连接,后面跟着的admin代表着该用户有哪些角色
roles空间就是一个角色对应着的权限,图中表示admin角色,可以有两种权限,user:*代表着可以对user类进行任意操作,agent:create代表可以对管理员进行创建操作。

单机程序代码

public class SingleShiroTest {
    public static void main(String[] args) {
        //通过Shiro提供的SecurityManager工厂类读取配置文件创建实例
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        SecurityManager securityManager = factory.getInstance();
        //设置一个securityManager
        SecurityUtils.setSecurityManager(securityManager);

        //获取当前需要操作程序的一个对象
        Subject subject = SecurityUtils.getSubject();
        String username = "climbingxiaowen";
        String password = "nihao";
        //生成令牌,即UsernamePasswordToken
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username,password);
        //对用户登录
        subject.login(usernamePasswordToken);

        //对用户进行权限校验
        subject.checkPermission("user:delete");

        subject.checkPermission("agent:delete");
    }
}

运行结果

在这里插入图片描述
程序顺利运行到了最后一行,检验是否有agent:delete权限,我们看到ini文件里是没有这个权限的,符合预期

程序执行过程

  1. 通过工厂读取配置文件并返回一个SecurityManager
  2. SecurityManager设置到SecurityUtils
  3. SecurityManager中获取一个Subject对象
  4. 根据UsernamePassword生成一个UsernamePasswordToken令牌
  5. 调用subject.login()函数进行登录验证
  6. subject对象进行权限判断

程序执行过程详解

我们没有配置Realm,凭什么知道账号密码能登录?

Realm的配置隐含在工厂类中,在工厂类中调用getInstance()的时候,会自动读取.ini配置文件,创建一个Realm,并把Realm放到SecurityManager中去,具体代码如下:

private SecurityManager createSecurityManager(Ini ini, Ini.Section mainSection) {

        getReflectionBuilder().setObjects(createDefaults(ini, mainSection));
        Map<String, ?> objects = buildInstances(mainSection);

        SecurityManager securityManager = getSecurityManagerBean();

        boolean autoApplyRealms = isAutoApplyRealms(securityManager);

        if (autoApplyRealms) {
            //realms and realm factory might have been created - pull them out first so we can
            //initialize the securityManager:
            Collection<Realm> realms = getRealms(objects);
            //set them on the SecurityManager
            if (!CollectionUtils.isEmpty(realms)) {
                applyRealmsToSecurityManager(realms, securityManager);
            }
        }

        return securityManager;
    }
  1. UsernamePasswordToken生成过程
    把传进来的username和password存储到新生成的token中,非常简单
    public UsernamePasswordToken(final String username, final char[] password,
                                 final boolean rememberMe, final String host) {

        this.username = username;
        this.password = password;
        this.rememberMe = rememberMe;
        this.host = host;
    }

login过程

重要!重要!重要!重要!重要!
将token传入login函数,调用顺序:
Subject subject = securityManager.login(this, token);将token传给DefaultSecurityManager.login
AuthenticationInfo info = uthenticate(token);DefaultSecurityManager将token传给自己的AuthenticatingSecurityManager.authenticate()
return this.authenticator.authenticate(token);AuthenticatingSecurityManager调用自己存储的authenticator进行验证
在这里插入图片描述
可以在这副图中看到很熟悉的两个身影,验证器和授权器下面都有一个realms,而这个realms就存储着我们读取配置文件生成的iniRealm
在这里插入图片描述
在这里插入图片描述
发现端倪了吗?其实iniRealm存储用户和角色和权限的方式,使用HashMap做一个映射,让用户映射到角色,让角色映射到权限。
在这里插入图片描述
authenticator是如何验证身份的呢?
先进入info = doAuthenticate(token);,判断有几个realm需要验证,

if (realms.size() == 1) {
           return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
       } else {
           return doMultiRealmAuthentication(realms, authenticationToken);
       }

如果只有一个realm需要验证就进入doSingleRealmAuthentication。
进入之后会判断,该realm是不是支持验证该token,每个realm有对应的支持token类型,token的类型有两种:
1、 UsernamePasswordToken
2、 BearerToken
两种Token的区别在于:
前者保存的是一对用户名和密码,后者保存的是一段token字符串,前者一般用于登录验证,后者一般是在发起Http请求带来的,比如后面整合前后端分离权限验证会用到的JWT token,不过我们在使用的时候也可以自己去实现Token类,但是需要记得在Realm里重写support方法来支持自己的Realm对Token进行验证!
判断完支持类型之后会调用AuthenticationInfo info = realm.getAuthenticationInfo(token);,进入该函数会首先从Realm缓存中读取是不是已经有缓存过这个token的信息,如果没有就进入关键的info = doGetAuthenticationInfo(token);因为iniRealm继承的是SimpleAccountRealm,所以调用的是SimpleAccountRealm的验证函数,我们在实现自己的Realm的时候需要重写这个doGetAuthenticationInfo方法,来支持自己的token判断,比如会在该函数从,调用Dao层方法,获取数据库存储的用户名和密码。
在SimpleAcountRealm中,验证权限的过程很简单,我们前面提到过Realm里保存了一张HashMap映射用来存储用户-角色,角色-权限的对应关系,在这里验证权限的方法就是从HashMap中根据用户名查找,如果能拿到对应的SimpleAccont对象就返回该对象。
返回的是一个AuthenticationInfo类型,也就是验证信息对象,拿到验证信息对象之后要把对象和token进行比对,上面拿到Info的过程就好像是一个人来到一家理发店说自己是这里的300号会员,店主看了下系统,发现确实有300号会员,然后店主说报下你的手机号,这个时候就是要进入检测credentials的过程。
进入assertCredentialsMatch(token, info);进行凭证检测,从token和info中获取Credentials,转换成Byte[]数组进入return MessageDigest.isEqual(tokenBytes, accountBytes);,进行比对,比对的过程很有意思,先上源码:

 for (int i = 0; i < lenA; i++) {
            // If i >= lenB, indexB is 0; otherwise, i.
            int indexB = ((i - lenB) >>> 31) * i;
            result |= digesta[i] ^ digestb[indexB];
        }
        return result == 0;

我们发现这是个时间恒定的比较,跟我们一般比较不同,如果让我来写可能会写成下面这个:

for(int i=0;i<digesta.length();i++){
	if(digesta[i]!=digestb[i]){
		return false;
	}
}
return true;

按照有源码方式写的比对好处在于不怕时间检测攻击,比如说一个人一直用不同的密码来校验,发现有的密码校验时间很短,一下子就失败了,有的密码校验时间比较长,说明校验到比较后面的位置。按照源码方式的比对,不管怎么校验都是需要校验完毕最后返回结果,避免了时间检测攻击。
如果比对成功,就返回一个authenticationInfo,期间没有抛出异常说明都成功,登录也就成功了!

权限验证过程

重要!重要!重要!重要!重要!
权限验证的过程大体上也和登录差不多,关键的对象是一个AuthorizationInfo,先调用this.authorizer.checkPermission(principals, permission);,进入验证权限函数,检查是否有Realm支持对该权限的验证,在单机程序中,系统默认配置的AuthorizingRealm显然是支持的,这里是层层嵌套调用,不去细说,说点关键部分:
1、把Permission表达式解析成Permission对象
一般我们使用的都是WildcardPermission对象,这个对象里保存了一个List<Set<String>> parts;属性用来存储权限表达式的三个部分,是通过new WildcardPermission的时候调用setParts()方法来生成,将字符串分割添加到parts中去,源码如下:

	List<String> parts = CollectionUtils.asList(wildcardString.split(PART_DIVIDER_TOKEN));

        this.parts = new ArrayList<Set<String>>();
        for (String part : parts) {
            Set<String> subparts = CollectionUtils.asSet(part.split(SUBPART_DIVIDER_TOKEN));

            if (subparts.isEmpty()) {
                throw new IllegalArgumentException("Wildcard string cannot contain parts with only dividers. Make sure permission strings are properly formatted.");
            }
            this.parts.add(subparts);
        }

创建好Permission之后,会进入我们非常熟悉的方法info = doGetAuthorizationInfo(principals);,这一步还是通过存储在Hashmap中的kv对来获取username对应的SimpleAccount信息,该对象存储了account的验证信息和权限信息。
拿到AuthorizationInfo信息之后,开始权限比对:

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;
 }

比对过程就是把传入的权限表达式也分解为parts,然后从AuthorizationInfo中拿到parts,一个个进行比对,如果有*通配符则跳过该part的比对。
至此权限验证结束。

登录流程总结

  1. 生成UsernamePasswordToken
  2. 传入authenticator(Realm)进行比对
  3. 调用doGetAuthenticationInfo()从Realm对象中获取验证信息
  4. AuthenticationInfoUsernamePasswordToken进行比对
  5. 比对成功or失败

鉴权流程总结

  1. 解析Permission表达式为Permission对象(分割字符串保存到partsList)
  2. 将Permission对象传入doGetAthorizationInfo函数
  3. 在函数内部比对字符串是否相符
  4. 返回结果

总结

Shiro框架使用起来非常简单,源码阅读难度也不大,是个简单易用的框架,整体核心部分就是弄清楚三大对象Subject,SecurityManager,Realm,以及验证和鉴权的关键过程,这对于以后我们自定义权限管理信息有很大帮助,可以在SecurityManager中添加拦截API的路径,在Realm里和数据库打通实现密码校验、权限检查等操作,下次会出一篇SpringBoot+Shiro+JWT Token+Mybatis的集成前后端分离Demo教学。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值