Shiro框架(原理分析与简单实现)
有兴趣的同学也可以阅读我之前分享的:Java权限管理(授权与认证)CRM权限管理 (PS : 这篇博客里面的实现方式没有使用框架,完全是手写的授权与认证,可以帮助理解Shiro框架)
https://www.cnblogs.com/yly-blog/p/7283541.html
如果发现分享的内容有不合理或者的不对地方,请留言,我会及时定位分析,感谢!!!
1.介绍
1.1 什么是权限管理?
基本上涉及到用户参与的系统都要进行权限管理,权限管理属于系统安全的范畴,权限管理实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。
权限管理包括用户身份认证和授权两部分,简称认证授权。对于需要访问控制的资源用户首先经过身份认证,认证通过后用户具有该资源的访问权限方可访问。
- 认证
身份认证,就是判断一个用户是否为合法用户的处理过程.最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确(登录).对于采用指纹等系统,则出示指纹;对于硬件Key等刷卡系统,则需要刷卡.
上边的流程图中需要理解以下关键对象:
Subject:主体 (user)(抽象的概念:保存登录后的相关信息)
访问系统的用户,主体可以是用户、程序等,进行认证的都称为主体;
Principal:身份信息 (username)
是主体(subject)进行身份认证的标识,标识必须具有唯一性,如用户名、手机号、邮箱地址等,一个主体可以有多个身份,但是必须有一个主身份(Primary Principal)(类似于数据库表中的id,保持唯一)。
credential:凭证信息 (password)
是只有主体自己知道的安全信息,如密码、证书等。
匿名访问:类似于淘宝首页
是否认证通过:淘宝下单,检查是否登录,没有登录请登录后下单
- 授权
授权,即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的。(前提:必须认证通过)
例如:假如你刚好看中了一个高清无码的种子,而你准备要下载了,这个时候会弹出一个非常恶心的框,让你登录才行,好,你辛辛苦苦注册登录后,点击下载,这时有弹出一个框,告诉你必须是VIP用户才能下载,这时候就是分配权限启最用,这时你就会去充值一个VIP,然后可以点击下载,这时他们就会检查你这个用户是否有权限,有权限下载,没有权限不允许下载。
1.2 什么是Apache Shiro?
Apache Shiro是Java的一个安全框架。目前,使用的人越来越多,因为它相当简单,对比Spri9ng Security,功能就没那么强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用小而简单的Shiro就足够了。这里我们就不纠结它俩哪个好哪个坏,能更简单的解决项目问题就好了。
Apache Shiro是Java的一个安全框架。帮助我们完成:认证、授权、加密、会话管理、与Web集成、缓存等。
1.3 从功能角度分析Shiro能做什么事情?
Shiro可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在JavaEE环境。Shiro可以帮助我们完成:认证、授权、加密、会话管理、与Web集成、缓存等。

Authentication:身份认证/登录,验证用户是不是拥有相应的身份;
Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;
Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
Web Support:Web支持,可以非常容易的集成到Web环境;
Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
Testing:提供测试支持;
Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
1.4 Shiro核心概念?
Shiro架构有三个主要概念 - Subject,SecurityManager,Realms。
下图仅供临时理解
Subject :访问系统的用户,主体可以是用户、程序等,进行认证的都称为主体;
Subject一词是一个安全术语,其基本意思是“当前的操作用户”。它是一个抽象的概念,可以是人,也可以是第三方进程或其他类似事物,如爬虫,机器人等。
在程序任意位置:Subject currentUser = SecurityUtils.getSubject(); 获取shiro
一旦获得Subject,你就可以立即获得你希望用Shiro为当前用户做的90%的事情,如登录、登出、访问会话、执行授权检查等
SecurityManager
安全管理器,它是shiro功能实现的核心,负责与后边介绍的其他组件(认证器/授权器/缓存控制器)进行交互,实现subject委托的各种功能。有点类似于spirngmvc中的DispatcherServlet前端控制器。
Realms
Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。;可以把Realm看成DataSource,即安全数据源。执行认证(登录)和授权(访问控制)时,Shiro会从应用配置的Realm中查找相关的比对数据。以确认用户是否合法,操作是否合理
- Subject
在考虑应用安全时,你最常问的问题可能是“当前用户是谁?”或“当前用户允许做X吗?”。当我们写代码或设计用户界面时,问自己这些问题很平常:应用通常都是基于用户故事构建的,并且你希望功能描述(和安全)是基于每个用户的。所以,对于我们而言,考虑应用安全的最自然方式就是基于当前用户。Shiro的API用它的Subject概念从根本上体现了这种思考方式。
Subject一词是一个安全术语,其基本意思是“当前的操作用户”。称之为“用户”并不准确,因为“用户”一词通常跟人相关。在安全领域,术语“Subject”可以是人,也可以是第三方进程或其他类似事物,如爬虫,机器人等。它仅仅意味着“当前跟软件交互的东西”。但考虑到大多数目的和用途,你可以把它认为是Shiro的“用户”概念。在代码的任何地方,你都能轻易的获得Shiro Subject。
Subject currentUser = SecurityUtils.getSubject();
一旦获得Subject,你就可以立即获得你希望用Shiro为当前用户做的90%的事情,如登录、登出、访问会话、执行授权检查等 - 稍后还会看到更多。这里的关键点是Shiro的API非常直观,因为它反映了开发者以‘每个用户’思考安全控制的自然趋势。同时,在代码的任何地方都能很轻松地访问Subject,允许在任何需要的地方进行安全操作。
- SecurityManager
Subject的“幕后”推手是SecurityManager。Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。它是Shiro框架的核心,充当“保护伞”,引用了多个内部嵌套安全组件,它们形成了对象图。但是,一旦SecurityManager及其内部对象图配置好,它就会退居幕后,应用开发人员几乎把他们的所有时间都花在Subject API调用上。
那么,如何设置SecurityManager呢?嗯,这要看应用的环境。例如,Web应用通常会在Web.xml中指定一个Shiro Servlet Filter,这会创建SecurityManager实例,如果你运行的是一个独立应用,你需要用其他配置方式,但有很多配置选项。
一个应用几乎总是只有一个SecurityManager实例。它实际是应用的Singleton。跟Shiro里几乎所有组件一样,SecurityManager的缺省实现是POJO,而且可用POJO兼容的任何配置机制进行配置 - 普通的Java代码、Spring XML、YAML、.properties和.ini文件等。基本来讲,能够实例化类和调用JavaBean兼容方法的任何配置形式都可使用。
可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器。
- Realms
Shiro的第三个也是最后一个概念是Realm。Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当切实与像用户帐户这类安全相关数据进行交互,执行认证(登录)和授权(访问控制)时,Shiro会从应用配置的Realm中查找很多内容。
从这个意义上讲,Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。当配置Shiro时,你必须至少指定一个Realm,用于认证和(或)授权。配置多个Realm是可以的,但是至少需要一个。
Realm 可以理解为读取用户信息、角色及权限的 DAO,就是说SecurityManager要验证用户身份与权限,那么它需要从Realm获取相应的信息进行比较以确定用户身份是否合法;可以把Realm看成DataSource,即安全数据源。
最简单的一个Shiro应用:
1、应用代码通过Subject来进行认证和授权,而Subject又委托给SecurityManager;
2、我们需要给Shiro的SecurityManager注入Realm,从而让SecurityManager能得到数据库中的用户及其权限进行判断;
1.5 从系统结构角度理解什么是权限管理?
简单理解下图流程:最上层代表其他程序,进来后,先进入Security Manager 进行授权或者认证(authenticator authorizer),而授权或者认证的数据是从最底层的DB中查询出来的,然后放到不同的realms中(realms中的这些值用来和程序中传入的数据(程序中传入的数据被封装成subject主题)进行比对。
Subject:主体,可以看到主体可以是任何可以与应用交互的“用户”;
SecurityManager:相当于SpringMVC中的DispatcherServlet或者Struts2中的FilterDispatcher;是Shiro的心脏;所有具体的交互都通过SecurityManager进行控制;它管理着所有Subject、且负责进行认证和授权、及会话、缓存的管理。
Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得Shiro默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
Authorizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
Realm:可以有1个或多个Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC实现,也可以是LDAP实现,或者内存实现等等;由用户提供;注意:Shiro不知道你的用户/权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的Realm;
SessionManager:如果写过Servlet就应该知道Session的概念,Session呢需要有人去管理它的生命周期,这个组件就是SessionManager;而Shiro并不仅仅可以用在Web环境,也可以用在如普通的JavaSE环境、EJB等环境;所有呢,Shiro就抽象了一个自己的Session来管理主体与应用之间交互的数据;可以实现分布式的会话管理;
SessionDAO:DAO大家都用过,数据访问对象,用于会话的CRUD,比如我们想把Session保存到数据库,那么可以实现自己的SessionDAO,通过如JDBC写到数据库;比如想把Session放到redis中,可以实现自己的redis SessionDAO;另外SessionDAO中可以使用Cache进行缓存,以提高性能;
CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能
Cryptography:密码模块,Shiro提高了一些常见的加密组件用于如密码加密/解密的。
2.认证
2.1 认证流程
2.2 使用ini完成认证
2.2.1 配置依赖
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.1.3</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.2.2</version> </dependency> |
2.2.2 添加shiro.ini配置文件
首先准备一些用户身份/凭据(shiro.ini):
[users] zhangsan=666 lisi=888 |
2.2.3 登录/退出
package com.ssm.test; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.config.IniSecurityManagerFactory; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.Factory; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ShiroTest { private final static Logger logger = LoggerFactory.getLogger(ShiroTest.class); @Test public void testLogin() { // 1、创建SecurityManager工厂对象:加载配置文件,创建工厂对象 Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); //2、创建SecurityManager SecurityManager securityManager = factory.getInstance(); //3、将securityManager设置到运行环境中:目的是让系统随时随地可以访问securityManger对象 SecurityUtils.setSecurityManager(securityManager); //4、创建当前登录的主体,注意:此时主体没有经过认证 Subject subject = SecurityUtils.getSubject(); //5、收集主体登录的身份 / 凭证 (创建token令牌),记录用户认证的身份和凭证即账号和密码 //参数1:将要登录的用户名,参数2:登录用户的密码 // 如果参数1出错 :抛异常 org.apache.shiro.authc.UnknownAccountException // 如果参数2出错 :抛异常org.apache.shiro.authc.IncorrectCredentialsException UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "666"); //6、主题登录 try { logger.info("主体登录"); subject.login(token); } catch (Exception e) { // 登录失败 System.out.println("testLogin exception value : " + e.getMessage()); System.out.println("testLogin exception value e : " + e); } //7、判断登录是否成功 System.out.println("验证登录是否成功 :" + subject.isAuthenticated()); //8、登出(注销) subject.logout(); System.out.println("验证登录是否成功 :" + subject.isAuthenticated()); } } |
登录常见异常 :
如果身份验证失败请捕获AuthenticationException或其子类,常见的如: DisabledAccountException(禁用的帐号)、LockedAccountException(锁定的帐号)、UnknownAccountException(错误的帐号)、ExcessiveAttemptsException(登录失败次数过多)、IncorrectCredentialsException (错误的凭证)、ExpiredCredentialsException(过期的凭证)等
2.2.4 执行流程分析
1、调用subject.login方法进行登录,其会自动委托给securityManager.login方法进行登录;
2、securityManager通过Authenticator(认证器)进行认证;
3、Authenticator的实现ModularRealmAuthenticator调用realm从ini配置文件取用户真实的账号和密码,这里使用的是IniRealm(shiro自带,相当于数据源);
4、IniRealm先根据token中的账号去ini中找该账号,如果找不到则给ModularRealmAuthenticator返回null,如果找到则匹配密码,匹配密码成功则认证通过。
5、最后调用Subject.logout进行退出操作。
2.2.5 存在的问题
1、用户名/密码硬编码在ini配置文件,以后需要改成如数据库存储,且密码需要加密存储;
2.3 Realm体系
Realm:域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。如我们之前的ini配置方式将使用org.apache.shiro.realm.text.IniRealm。
org.apache.shiro.realm.Realm接口如下:
------------------------------------------------------------------------------------------------------------------------------------------------------
String getName(); //返回一个唯一的Realm名字
boolean supports(AuthenticationToken token); //判断此Realm是否支持此Token
AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException; //根据Token获取认证信息
Realm体系:看图.
最基础的是Realm接口,CachingRealm负责缓存处理,AuthenticationRealm负责认证,AuthorizingRealm负责授权,通常自定义的realm继承AuthorizingRealm。
2.3.1 自定义Realm
1. 自定义realm,继承AuthorizingRealm 重写3个方法:getName doGetAuthorizationInfo doGetAuthenticaionInfo
2. 配置ini文件,指定使用自定义realm
3. 加载配置文件shiro-realm.ini,执行登录操作
package com.ssm.realm; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; public class MyRealm extends AuthorizingRealm { // 一个系统可能有不同的realm,为了区分使用 @Override public String getName() { return "MyRealm"; } // 授权操作 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { // TODO Auto-generated method stub return null; } // 认证操作 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("token value :" + token); // 参数token:表示登录时包装的UsernamePasswordToken // 通过用户名到数据库中查询用户信息,封装成一个AuthenticationInfo对象返回,方便认证器进行对比 // 获取token中的用户名 String username = (String) token.getPrincipal(); // 通过用户名查询数据库,将该用户对应数据查询返回:账号与密码 // 假设查询数据库返回数据时:zhangsan 666 if(!"zhangsan".equals(username)) { return null; } String password = "666"; // info对象表示realm登录比对信息:参数1:用户信息(真实登录中是登录对象user对象),参数2:密码,参数3:当前realm名字 SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, password, getName()); System.out.println("info value :" + info); return info; } } |
2.3.1 添加shiro-realm.ini配置文件
ini配置文件指定自定义Realm实现(shiro-realm.ini) #声明一个realm myRealm=com.ssm.realm.MyRealm #指定securityManager的realms实现 securityManager.realms=$myRealm |
2.3.1 登录/退出
@Test public void testLoginByRealm() { // 1、创建SecurityManager工厂对象:加载配置文件,创建工厂对象 Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-realm.ini"); //2、创建SecurityManager SecurityManager securityManager = factory.getInstance(); //3、将securityManager设置到运行环境中:目的是让系统随时随地可以访问securityManger对象 SecurityUtils.setSecurityManager(securityManager); //4、创建当前登录的主体,注意:此时主体没有经过认证 Subject subject = SecurityUtils.getSubject(); //5、收集主体登录的身份 / 凭证 (创建token令牌),记录用户认证的身份和凭证即账号和密码 //参数1:将要登录的用户名,参数2:登录用户的密码 // 如果参数1出错 :抛异常 org.apache.shiro.authc.UnknownAccountException // 如果参数2出错 :抛异常org.apache.shiro.authc.IncorrectCredentialsException UsernamePasswordToken token = new UsernamePasswordToken("zhangsa1n", "666"); //6、主题登录 try { logger.info("主体登录"); subject.login(token); } catch (Exception e) { // 登录失败 e.printStackTrace(); //System.out.println("testLogin exception value : " + e.getMessage()); //System.out.println("testLogin exception value e : " + e); } //7、判断登录是否成功 System.out.println("验证登录是否成功 :" + subject.isAuthenticated()); //8、登出(注销) subject.logout(); System.out.println("验证登录是否成功 :" + subject.isAuthenticated()); } |
2.4 使用Realm进行密码散列
在涉及到密码存储问题上,应该加密/生成密码摘要存储,而不是存储明文密码。比如之前的600w csdn账号泄露对用户可能造成很大损失,因此应加密/生成不可逆的摘要方式存储。
2.4.1 散列算法
散列算法一般用于生成数据的摘要信息,是一种不可逆的算法,一般适合存储密码之类的数据,常见的散列算法如MD5、SHA等。一般进行散列时最好提供一个salt(盐),比如加密密码“admin”,产生的散列值是“21232f297a57a5a743894a0e4a801fc3”,可以到一些md5解密网站很容易的通过散列值得到密码“admin”,即如果直接对密码进行散列相对来说破解更容易,此时我们可以加一些只有系统知道的干扰数据,如用户名和ID(即盐);这样散列的对象是“密码+用户名+ID”,这样生成的散列值相对来说更难破解。
package com.ssm.test; import org.apache.shiro.crypto.hash.Md5Hash; import org.junit.Test; public class MD5Test { @Test public void testMD5() throws Exception { String password = "666"; // 密码:明文 // 加密:md5 Md5Hash md5Hash = new Md5Hash(password); System.out.println(md5Hash); // fae0b27c451c728867a567e8c1bb4e53 // 加密:md5 + 盐salt md5Hash = new Md5Hash(password, "zhangsan"); System.out.println(md5Hash); // 2f1f526e25fdefa341c7a302b47dd9df // 加密:md5 + 盐salt + 散列次数 md5Hash = new Md5Hash(password, "zhangsan", 3); System.out.println(md5Hash); // cd757bae8bd31da92c6b14c235668091 } } |
2.4.2 在Realm中应用
实际应用是将盐和散列后的值存在数据库中,自动realm从数据库取出盐和加密后的值由shiro完成密码校验。
1. 自定义加密之后realm,重写3个方法
2. 配置ini配置文件
3. 加载配置文件,测试
2.4.2.1 自定义Realm
package com.ssm.realm; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource; public class PasswordRealm extends AuthorizingRealm { @Override public String getName() { return "PasswordRealm"; } // 授权操作 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { return null; } // 认证操作 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("token value :" + token); // 参数token:表示登录时包装的UsernamePasswordToken // 通过用户名到数据库中查询用户信息,封装成一个AuthenticationInfo对象返回,方便认证器进行对比 // 获取token中的用户名 String username = (String) token.getPrincipal(); // 通过用户名查询数据库,将该用户对应数据查询返回:账号与密码 // 假设查询数据库返回数据时:zhangsan 666 if(!"zhangsan".equals(username)) { return null; } // 模拟数据库中保存加密之后密文:666 + 账号 + 散列次数 String password = "cd757bae8bd31da92c6b14c235668091"; // info对象表示realm登录比对信息:参数1:用户信息(真实登录中是登录对象user对象),参数2:密码,参数3:盐,参数4:当前realm名字 SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, password, ByteSource.Util.bytes("zhangsan"), getName()); System.out.println("info value :" + info); return info; } } |
2.4.2.2 添加shiro-cryptography.ini配置文件
[main] #定义凭证匹配器 credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher #散列算法 credentialsMatcher.hashAlgorithmName=md5 #散列次数 credentialsMatcher.hashIterations=3 #将凭证匹配器设置到realm myRealm=com.ssm.realm.PasswordRealm myRealm.credentialsMatcher=$credentialsMatcher securityManager.realms=$myRealm |
2.4.2.3 登录/退出
@Test public void testLoginByPasswordRealm() { // 1、创建SecurityManager工厂对象:加载配置文件,创建工厂对象 Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-cryptography.ini"); //2、创建SecurityManager SecurityManager securityManager = factory.getInstance(); //3、将securityManager设置到运行环境中:目的是让系统随时随地可以访问securityManger对象 SecurityUtils.setSecurityManager(securityManager); //4、创建当前登录的主体,注意:此时主体没有经过认证 Subject subject = SecurityUtils.getSubject(); //5、收集主体登录的身份 / 凭证 (创建token令牌),记录用户认证的身份和凭证即账号和密码 //参数1:将要登录的用户名,参数2:登录用户的密码 // 如果参数1出错 :抛异常 org.apache.shiro.authc.UnknownAccountException // 如果参数2出错 :抛异常org.apache.shiro.authc.IncorrectCredentialsException UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "666"); //6、主题登录 try { logger.info("主体登录"); subject.login(token); } catch (Exception e) { // 登录失败 e.printStackTrace(); //System.out.println("testLogin exception value : " + e.getMessage()); //System.out.println("testLogin exception value e : " + e); } //7、判断登录是否成功 System.out.println("验证登录是否成功 :" + subject.isAuthenticated()); //8、登出(注销) subject.logout(); System.out.println("验证登录是否成功 :" + subject.isAuthenticated()); } |
3.授权
3.1 授权模式:RBAC
shiro 授权(权限控制)实现是居于RBAC而实现的
谁(user)扮演什么角色(role), 可以做什么事情(permission)
RBAC: 基于角色的权限管理
简单理解为:谁扮演什么角色, 被允许做什么操作
用户对象:user: 当前操作用户
角色对象:role:表示权限操作许可权的集合
权限对象:permission: 资源操作许可权
例子:张三(user) 下载(permission)一个高清无码的种子(资源), 需要VIP权限(role)
张三--->普通用户--->授权---->VIP用户----->下载种子
3.2 授权流程
3.3 授权方式
Shiro支持三种方式的授权
3.3.1 编程式
通过写if/else授权代码块完成
Subject subject = SecurityUtils.getSubject();
if(subject.hasRole(“admin”)) {
//有权限
} else {
//无权限
}
3.3.2 注解式
通过在执行的Java方法上放置相应的注解完成
@RequiresRoles("admin")
public void hello() {
//有权限
}
3.3.3 JSP标签
在JSP页面通过相应的标签完成
<shiro:hasRole name="admin">
<!— 有权限 —>
</shiro:hasRole>
3.4 使用ini完成授权
权限表达式定义如下:
在ini文件中用户、角色、权限的配置规则是:“用户名=密码,角色1,角色2...” “角色=权限1,权限2...”,首先根据用户名找角色,再根据角色找权限,角色是权限集合。
权限字符串的规则是:“资源标识符:操作:资源实例标识符”,意思是对哪个资源的哪个实例具有什么操作,“:”是资源/操作/实例的分割符,权限字符串也可以使用*通配符。
例子:
用户创建权限:user:create,或user:create:*
用户修改实例001的权限:user:update:001
用户实例001的所有权限:user:*:001
一般而已,我们操作只需要关注前面两节:
资源:操作 :
*:* : 所有资源的所有操作权限--->admin
3.4.1 添加shiro-permission.ini配置文件
[users] #用户zhang的密码是123,此用户具有role1和role2两个角色 zhangsan=666,role1,role2 lisi=888,role2 [roles] #角色role1对资源user拥有create、update权限 role1=user:create,user:update #角色role2对资源user拥有create、delete权限 role2=user:create,user:delete #角色role3对资源user拥有create权限 role3=user:create |
字符串规则
在ini文件中用户、角色、权限的配置规则是:“用户名=密码,角色1,角色2...” “角色=权限1,权限2...”,首先根据用户名找角色,再根据角色找权限,角色是权限集合。 权限字符串的规则是:“资源标识符:操作:资源实例标识符”,意思是对哪个资源的哪个实例具有什么操作,“:”是资源/操作/实例的分割符,权限字符串也可以使用*通配符。 例子: 用户创建权限:user:create,或user:create:* 用户修改实例001的权限:user:update:001 用户实例001的所有权限:user:*:001 |
3.4.2 角色测试
@Test public void testHasRole() throws Exception { Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-permission.ini"); SecurityManager securityManager = factory.getInstance(); SecurityUtils.setSecurityManager(securityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "666"); subject.login(token); // 进行授权操作时前提:用户必须通过认证 // 判断当前用户是否拥有某个角色 System.out.println(subject.hasRole("role1")); // 判断当前用户是否拥有一些角色:返回true表示全部拥有,false 表示不全部拥有 System.out.println(subject.hasAllRoles(Arrays.asList("role1", "role2"))); // 判断当前用户是否拥有一些角色:返回boolean类型数据,true表示拥有某个角色,false表示没有 System.out.println(Arrays.toString(subject.hasRoles(Arrays.asList("role1", "role2")))); // 判断当前用户是否拥有一些角色:没有返回值, 如果有角色,不做任何操作,没有报异常:org.apache.shiro.authz.UnauthorizedException subject.checkRole("role1"); try { // 判断当前用户是否拥有一些角色 subject.checkRoles("role1", "role2"); } catch (Exception e) { e.printStackTrace(); } } |
3.4.3 权限测试
@Test public void testHasRole() throws Exception { Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-permission.ini"); SecurityManager securityManager = factory.getInstance(); SecurityUtils.setSecurityManager(securityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "666"); subject.login(token); // 进行授权操作时前提:用户必须通过认证 // 判断当前用户是否拥有某个权限,返回true表示拥有指定权限,false表示没有 System.out.println(subject.isPermitted("user:delete")); // 判断当前用户是否拥有一些权限,返回true表示都拥有,返回false表示不全拥有 System.out.println(subject.isPermittedAll("user:list", "user:delete")); // 判断当前用户是否拥有一些权限,返回boolean数组,true表示有false表示没有 System.out.println(Arrays.toString(subject.isPermitted("user:list", "user:delete"))); // 判断当前用户是否拥有某个权限,没有返回值,如果没有拥有指定权限报异常:org.apache.shiro.authz.UnauthorizedException try { subject.checkPermission("user:crea1te"); } catch (Exception e) { e.printStackTrace(); } // 判断当前用户是否拥有某个角色 //System.out.println(subject.hasRole("role1")); // 判断当前用户是否拥有一些角色:返回true表示全部拥有,false 表示不全部拥有 //System.out.println(subject.hasAllRoles(Arrays.asList("role1", "role2"))); // 判断当前用户是否拥有一些角色:返回boolean类型数据,true表示拥有某个角色,false表示没有 //System.out.println(Arrays.toString(subject.hasRoles(Arrays.asList("role1", "role2")))); // 判断当前用户是否拥有一些角色:没有返回值, 如果有角色,不做任何操作,没有报异常:org.apache.shiro.authz.UnauthorizedException subject.checkRole("role1"); /*try { // 判断当前用户是否拥有一些角色 subject.checkRoles("role1", "role2"); } catch (Exception e) { e.printStackTrace(); }*/ } |
3.5 自定义Realm完成授权
3.5.1 自定义Realm
package com.ssm.realm; import java.util.ArrayList; import java.util.List; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; public class PermissionRealm extends AuthorizingRealm { // 一个系统可能有不同的realm,为了区分使用 @Override public String getName() { return "PermissionRealm"; } // 授权操作 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { // 传入参数:principals : 用户认证凭证信息: // SimpleAuthenticationInfo:认证方法返回封装认证信息中第一个参数:用户信息(username) // 当前登录用户名信息:用户凭证 String username = (String) principals.getPrimaryPrincipal(); // 模拟查询数据库:查询用户实现指定的角色,以及用户权限 List<String> roles = new ArrayList<String>(); // 角色集合 List<String> permission = new ArrayList<String>(); // 权限集合 //假设用户在数据库中有role1角色 roles.add("role1"); //假设用户在数据库中拥有user:delete权限 //permission.add("user:delete"); permission.add("user:*"); // 返回用户在数据库中的权限与角色 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.addRoles(roles); info.addStringPermissions(permission); return info; } // 认证操作 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("token value :" + token); // 参数token:表示登录时包装的UsernamePasswordToken // 通过用户名到数据库中查询用户信息,封装成一个AuthenticationInfo对象返回,方便认证器进行对比 // 获取token中的用户名 String username = (String) token.getPrincipal(); // 通过用户名查询数据库,将该用户对应数据查询返回:账号与密码 // 假设查询数据库返回数据时:zhangsan 666 if(!"zhangsan".equals(username)) { return null; } String password = "666"; // info对象表示realm登录比对信息:参数1:用户信息(真实登录中是登录对象user对象),参数2:密码,参数3:当前realm名字 SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, password, getName()); System.out.println("info value :" + info); return info; } } |
3.5.2 添加shiro-permission-realm.ini配置文件
[main] #声明一个realm myReal=com.ssm.realm.PermissionRealm #指定securityManager的realms实现 securityManager.realms=$myReal |
3.5.3 角色测试
@Test public void testHasRoleByRealm() throws Exception { Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-permission-realm.ini"); SecurityManager securityManager = factory.getInstance(); SecurityUtils.setSecurityManager(securityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "666"); subject.login(token); // 进行授权操作时前提:用户必须通过认证 // 判断当前用户是否拥有某个权限,返回true表示拥有指定权限,false表示没有 System.out.println("000"+subject.isPermitted("user:delete")); // 判断当前用户是否拥有某个角色 System.out.println("111"+subject.hasRole("role1")); } |
3.5.4 权限测试
@Test public void testHasRole() throws Exception { Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-permission.ini"); SecurityManager securityManager = factory.getInstance(); SecurityUtils.setSecurityManager(securityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "666"); subject.login(token); // 进行授权操作时前提:用户必须通过认证 // 判断当前用户是否拥有某个权限,返回true表示拥有指定权限,false表示没有 System.out.println(subject.isPermitted("user:delete")); // 判断当前用户是否拥有一些权限,返回true表示都拥有,返回false表示不全拥有 System.out.println(subject.isPermittedAll("user:list", "user:delete")); // 判断当前用户是否拥有一些权限,返回boolean数组,true表示有false表示没有 System.out.println(Arrays.toString(subject.isPermitted("user:list", "user:delete"))); // 判断当前用户是否拥有某个权限,没有返回值,如果没有拥有指定权限报异常:org.apache.shiro.authz.UnauthorizedException try { subject.checkPermission("user:crea1te"); } catch (Exception e) { e.printStackTrace(); } // 判断当前用户是否拥有某个角色 //System.out.println(subject.hasRole("role1")); // 判断当前用户是否拥有一些角色:返回true表示全部拥有,false 表示不全部拥有 //System.out.println(subject.hasAllRoles(Arrays.asList("role1", "role2"))); // 判断当前用户是否拥有一些角色:返回boolean类型数据,true表示拥有某个角色,false表示没有 //System.out.println(Arrays.toString(subject.hasRoles(Arrays.asList("role1", "role2")))); // 判断当前用户是否拥有一些角色:没有返回值, 如果有角色,不做任何操作,没有报异常:org.apache.shiro.authz.UnauthorizedException subject.checkRole("role1"); /*try { // 判断当前用户是否拥有一些角色 subject.checkRoles("role1", "role2"); } catch (Exception e) { e.printStackTrace(); }*/ } |
3.6 授权流程分析
1、首先调用Subject.isPermitted*/hasRole*接口,其会委托给SecurityManager,而SecurityManager接着会委托给Authorizer
2、Authorizer是真正的授权者,如果我们调用如isPermitted(“user:view”),其首先会通过PermissionResolver把字符串转换成相应的Permission实例;
3、在进行授权之前,其会调用相应的Realm获取Subject相应的角色/权限用于匹配传入的角色/权限;
4、Authorizer会判断Realm的角色/权限是否和传入的匹配,如果有多个Realm,会委托给ModularRealmAuthorizer进行循环判断,如果匹配如isPermitted*/hasRole*会返回true,否则返回false表示授权失败。