轻量级权限处理框架--Shiro

  • 前言
  • Shiro三大对象
    • Subject
    • SecurityManager
    • Realm
  • Authentication和Authorization
    • Authentication
    • Authorization
    • Authentication和Authorization小结
  • 权限控制的三个基本要素
    • 用户
    • 角色
    • 权限
      • 权限表达式是如何工作的呢?
  • Shiro单机示例
      • 解读ini配置文件
      • 单机程序代码
      • 运行结果
      • 程序执行过程
      • 程序执行过程详解
      • 我们没有配置Realm,凭什么知道账号密码能登录?
      • **login过程**
      • 权限验证过程
    • 登录流程总结
    • 鉴权流程总结
  • 总结

前言

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();//设置一个securityManagerSecurityUtils.setSecurityManager(securityManager);//获取当前需要操作程序的一个对象Subject subject = SecurityUtils.getSubject();String username = "climbingxiaowen";String password = "nihao";//生成令牌,即UsernamePasswordTokenUsernamePasswordToken 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 SecurityManagerif (!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教学。

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

  1. ELK系列(十五)、Elasticsearch核心原理一篇全搞定

    目录 Lucene 介绍 核心术语 如何理解倒排索引? 检索方式 分段存储 段合并策略 Elasticsearch 核心概念 节点类型 集群状态 3C和脑裂 1.共识性(Consensus) 2.并发 ...

  2. zabbix监控哪些东西_监控系统选型,一篇全搞定

    之前,写过几篇有关线上问题排查的文章,文中附带了一些监控图,有些读者对此很感兴趣,问我监控系统选型上有没有好的建议? 图片来自 Pexels 目前我所经历的几家公司,监控系统都是自研的.其实业界有很多 ...

  3. SpringBoot 就这一篇全搞定

    一.Hello Spring Boot 1.Spring Boot 简介 简化Spring应用开发的一个框架: 整个Spring技术栈的一个大整合: J2EE开发的一站式解决方案: 2.微服务 微服务 ...

  4. 从头撸到脚,SpringBoot 就一篇全搞定!

    一.Hello Spring Boot 1.Spring Boot 简介 简化Spring应用开发的一个框架: 整个Spring技术栈的一个大整合: J2EE开发的一站式解决方案: 2.微服务 微服务 ...

  5. 面试问Kafka,这一篇全搞定

    应大部分的小伙伴的要求,今天这篇咱们用大白话带你认识 Kafka Kafka基础 消息系统的作用 大部分小伙伴应该都清楚,这里用机油装箱举个例子: 所以消息系统就是如上图我们所说的仓库,能在中间过程作 ...

  6. 爬虫技术——一篇全搞定!

    目录: 目录 目录: 1. 爬虫介绍 1.1 爬虫是什么 1.2 爬虫步骤 1.3 爬虫分类 1.3.1 通用爬虫 1.3.2 聚焦爬虫 ​编辑 1.4 一些常见的反爬手段 2. Urllib 2.1 ...

  7. 开关电源波纹的产生、测量及抑制,一篇全搞定!

    开关电源纹波的产生 我们最终的目的是要把输出纹波降低到可以忍受的程度,达到这个目的最根本的解决方法就是要尽量避免纹波的产生,首先要清楚开关电源纹波的种类和产生原因. 上图是开关电源中最简单的拓扑结构- ...

  8. Qt5 pyqt5图片编辑器功能函数一篇全搞定:实现图片格式转换、显示、缩放、特效处理(模糊、锐化,浮雕等等)

    在本篇的基础上,你可以轻松实现一个自己的图片编辑器哦. 无论是图片格式的转换,还是各种效果的展示,基本和目前我们常用的图片编辑器功能雷同了. 至于pyqt5界面的编写,大家可以查看我的另外几篇文章,或 ...

  9. android+延迟拍摄,延时摄影很难吗? iphone拍+后期全搞定

    手机也能拍出大片,还是目前高端大气的延时摄影,这听起来有点儿不可思议!但如果你的智能手机支持延时摄影拍摄,你还真可以用手机拍大片,甚至说后期都全靠手机来制作.不信你且看我娓娓道来. 在生物演变.天体运 ...

最新文章

  1. 描述文件_【iOS】描述文件删除不了?教你一键移除所有恶意描述文件
  2. Node.js Express 框架 GET方法
  3. 测试社交软件有哪些,性格测试:测你适合哪个社交平台
  4. html调用rpst 源码_parseHTML 函数源码解析(四) AST 基本形成
  5. 《MFC 控件透明处理》
  6. P3537 [POI2012]SZA-Cloakroom
  7. 天涯“大鹏金翅明王”语录
  8. 微信公众平台实现天气预报功能
  9. eclipse代码:1到100既是3又是5的倍数
  10. HTML 樱花飘落界面效果
  11. 安装Visual Studio失败 返回代码:1603
  12. 如何使用python视频_如何使用python网络爬虫抓取视频?
  13. 使用idea进行远程调试
  14. EFI系统分区必须挂载到/boot/efi其中之一
  15. ZZULIOJ:1098: 复合函数求值(函数专题)
  16. python处理csv数据-分分快3大小
  17. aliyun 阿里云mysql备份恢复到本地环境
  18. 有了智能名片你也可以轻松投放信息流广告
  19. 微信小程序个人开发心得
  20. web端 网页端分享功能的实现

热门文章

  1. outlook2016关闭时最小化到任务栏的完美解决方法
  2. excel流程图分叉 合并_快速制作组织架构图,层次结构图,流程图等,只需学会这个功能...
  3. K3s - 安装部署
  4. ubuntu16.04 配置远程桌面
  5. C语言基础级——标准输入和输出
  6. 元学习之《On First-Order Meta-Learning Algorithms》论文详细解读
  7. android紫禁城一日游的代码,故宫旅游app下载-故宫旅游 安卓版v3.3.6-PC6安卓网
  8. 高德地图的测距api应用记录
  9. 梧桐树金玉满堂增额终身寿险将下架,百度开屏也懂我的资产荒焦虑
  10. JAVA毕业设计酒店管理系统设计与实现计算机源码+lw文档+系统+调试部署+数据库