一看就会!一篇全搞定!权限处理专家--Shiro保姆式教学,超详细!
轻量级权限处理框架--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文件里是没有这个权限的,符合预期
程序执行过程
- 通过工厂读取配置文件并返回一个
SecurityManager
- 将
SecurityManager
设置到SecurityUtils
中 - 从
SecurityManager
中获取一个Subject
对象 - 根据
Username
和Password
生成一个UsernamePasswordToken
令牌 - 调用
subject.login()
函数进行登录验证 - 对
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;}
- 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的比对。
至此权限验证结束。
登录流程总结
- 生成
UsernamePasswordToken
- 传入
authenticator(Realm)
进行比对 - 调用
doGetAuthenticationInfo()
从Realm对象中获取验证信息 AuthenticationInfo
和UsernamePasswordToken
进行比对- 比对成功or失败
鉴权流程总结
- 解析Permission表达式为
Permission
对象(分割字符串保存到partsList) - 将Permission对象传入
doGetAthorizationInfo
函数 - 在函数内部比对字符串是否相符
- 返回结果
总结
Shiro框架使用起来非常简单,源码阅读难度也不大,是个简单易用的框架,整体核心部分就是弄清楚三大对象Subject
,SecurityManager
,Realm
,以及验证和鉴权的关键过程,这对于以后我们自定义权限管理信息有很大帮助,可以在SecurityManager中添加拦截API的路径,在Realm里和数据库打通实现密码校验、权限检查等操作,下次会出一篇SpringBoot+Shiro+JWT Token+Mybatis的集成前后端分离Demo
教学。
一看就会!一篇全搞定!权限处理专家--Shiro保姆式教学,超详细!相关推荐
- ELK系列(十五)、Elasticsearch核心原理一篇全搞定
目录 Lucene 介绍 核心术语 如何理解倒排索引? 检索方式 分段存储 段合并策略 Elasticsearch 核心概念 节点类型 集群状态 3C和脑裂 1.共识性(Consensus) 2.并发 ...
- zabbix监控哪些东西_监控系统选型,一篇全搞定
之前,写过几篇有关线上问题排查的文章,文中附带了一些监控图,有些读者对此很感兴趣,问我监控系统选型上有没有好的建议? 图片来自 Pexels 目前我所经历的几家公司,监控系统都是自研的.其实业界有很多 ...
- SpringBoot 就这一篇全搞定
一.Hello Spring Boot 1.Spring Boot 简介 简化Spring应用开发的一个框架: 整个Spring技术栈的一个大整合: J2EE开发的一站式解决方案: 2.微服务 微服务 ...
- 从头撸到脚,SpringBoot 就一篇全搞定!
一.Hello Spring Boot 1.Spring Boot 简介 简化Spring应用开发的一个框架: 整个Spring技术栈的一个大整合: J2EE开发的一站式解决方案: 2.微服务 微服务 ...
- 面试问Kafka,这一篇全搞定
应大部分的小伙伴的要求,今天这篇咱们用大白话带你认识 Kafka Kafka基础 消息系统的作用 大部分小伙伴应该都清楚,这里用机油装箱举个例子: 所以消息系统就是如上图我们所说的仓库,能在中间过程作 ...
- 爬虫技术——一篇全搞定!
目录: 目录 目录: 1. 爬虫介绍 1.1 爬虫是什么 1.2 爬虫步骤 1.3 爬虫分类 1.3.1 通用爬虫 1.3.2 聚焦爬虫 编辑 1.4 一些常见的反爬手段 2. Urllib 2.1 ...
- 开关电源波纹的产生、测量及抑制,一篇全搞定!
开关电源纹波的产生 我们最终的目的是要把输出纹波降低到可以忍受的程度,达到这个目的最根本的解决方法就是要尽量避免纹波的产生,首先要清楚开关电源纹波的种类和产生原因. 上图是开关电源中最简单的拓扑结构- ...
- Qt5 pyqt5图片编辑器功能函数一篇全搞定:实现图片格式转换、显示、缩放、特效处理(模糊、锐化,浮雕等等)
在本篇的基础上,你可以轻松实现一个自己的图片编辑器哦. 无论是图片格式的转换,还是各种效果的展示,基本和目前我们常用的图片编辑器功能雷同了. 至于pyqt5界面的编写,大家可以查看我的另外几篇文章,或 ...
- android+延迟拍摄,延时摄影很难吗? iphone拍+后期全搞定
手机也能拍出大片,还是目前高端大气的延时摄影,这听起来有点儿不可思议!但如果你的智能手机支持延时摄影拍摄,你还真可以用手机拍大片,甚至说后期都全靠手机来制作.不信你且看我娓娓道来. 在生物演变.天体运 ...
最新文章
- 描述文件_【iOS】描述文件删除不了?教你一键移除所有恶意描述文件
- Node.js Express 框架 GET方法
- 测试社交软件有哪些,性格测试:测你适合哪个社交平台
- html调用rpst 源码_parseHTML 函数源码解析(四) AST 基本形成
- 《MFC 控件透明处理》
- P3537 [POI2012]SZA-Cloakroom
- 天涯“大鹏金翅明王”语录
- 微信公众平台实现天气预报功能
- eclipse代码:1到100既是3又是5的倍数
- HTML 樱花飘落界面效果
- 安装Visual Studio失败 返回代码:1603
- 如何使用python视频_如何使用python网络爬虫抓取视频?
- 使用idea进行远程调试
- EFI系统分区必须挂载到/boot/efi其中之一
- ZZULIOJ:1098: 复合函数求值(函数专题)
- python处理csv数据-分分快3大小
- aliyun 阿里云mysql备份恢复到本地环境
- 有了智能名片你也可以轻松投放信息流广告
- 微信小程序个人开发心得
- web端 网页端分享功能的实现
热门文章
- outlook2016关闭时最小化到任务栏的完美解决方法
- excel流程图分叉 合并_快速制作组织架构图,层次结构图,流程图等,只需学会这个功能...
- K3s - 安装部署
- ubuntu16.04 配置远程桌面
- C语言基础级——标准输入和输出
- 元学习之《On First-Order Meta-Learning Algorithms》论文详细解读
- android紫禁城一日游的代码,故宫旅游app下载-故宫旅游 安卓版v3.3.6-PC6安卓网
- 高德地图的测距api应用记录
- 梧桐树金玉满堂增额终身寿险将下架,百度开屏也懂我的资产荒焦虑
- JAVA毕业设计酒店管理系统设计与实现计算机源码+lw文档+系统+调试部署+数据库