和你们分享一个松哥原创的 Shiro 教程吧,还没写完,先整一部分,剩下的敬请期待。html
Apache Shiro是一个开源安全框架,提供身份验证、受权、密码学和会话管理。Shiro框架具备直观、易用等特性,同时也能提供健壮的安全性,虽然它的功能不如SpringSecurity那么强大,可是在普通的项目中也够用了。java
Shiro的前身是JSecurity,2004年,Les Hazlewood和Jeremy Haile创办了Jsecurity。当时他们找不到适用于应用程序级别的合适Java安全框架,同时又对JAAS很是失望。2004年到2008年期间,JSecurity托管在SourceForge上,贡献者包括Peter Ledbrook、Alan Ditzel和Tim Veil。2008年,JSecurity项目贡献给了Apache软件基金会(ASF),并被接纳成为Apache Incubator项目,由导师管理,目标是成为一个顶级Apache项目。期间,Jsecurity曾短暂改名为Ki,随后因商标问题被社区改名为“Shiro”。随后项目持续在Apache Incubator中孵化,并增长了贡献者Kalle Korhonen。2010年7月,Shiro社区发布了1.0版,随后社区建立了其项目管理委员会,并选举Les Hazlewood为主席。2010年9月22日,Shrio成为Apache软件基金会的顶级项目(TLP)。mysql
Apache Shiro是一个强大而灵活的开源安全框架,它干净利落地处理身份认证,受权,企业会话管理和加密。Apache Shiro的首要目标是易于使用和理解。安全有时候是很复杂的,甚至是痛苦的,但它没有必要这样。框架应该尽量掩盖复杂的地方,露出一个干净而直观的API,来简化开发人员在应用程序安全上所花费的时间。git
如下是你能够用Apache Shiro 所作的事情:github
为没有关联到登陆的用户启用"Remember Me"服务web
等等
Apache Shiro是一个拥有许多功能的综合性的程序安全框架。下面的图表展现了Shiro的重点:算法
Shiro中有四大基石——身份验证,受权,会话管理和加密。spring
除此以外,Shiro也提供了额外的功能来解决在不一样环境下所面临的安全问题,尤为是如下这些:sql
要学习shiro,咱们首先需求去shiro官网下载shiro,官网地址地址https://shiro.apache.org/,截... 在 2017-2019 曾经停更了两年,我一度觉得觉得这个项目 gg 了),本文将采用这个版本。固然,shiro咱们也能够从github上下载到源码。两个源码下载地址以下:数据库
上面我主要是和小伙伴们介绍下源码的下载,并无涉及到jar包的下载,jar包咱们到时候直接使用maven便可。
这里咱们先不急着写代码,咱们先打开刚刚下载到的源码,源码中有一个samples目录,以下:
这个samples目录是官方给咱们的一些演示案例,其中有一个quickstart项目,这个项目是一个maven项目,参考这个quickstart,咱们来建立一个本身的演示工程。
1.首先使用maven建立一个JavaSE工程
工程建立成功后在pom文件中添加以下依赖:
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-all</artifactId> <version>RELEASE</version> </dependency>
2.配置用户
参考quickstart项目中的shiro.ini文件,咱们来配置一个用户,配置方式以下:首先在resources目录下建立一个shiro.ini文件,文件内容以下:
[users] sang=123,admin [roles] admin=*
以上配置表示咱们建立了一个名为sang的用户,该用户的密码是123,该用户的角色是admin,而admin具备操做全部资源的权限。
3.执行登陆
OK,作完上面几步以后,咱们就能够来看看如何实现一次简单的登陆操做了。这个登陆操做咱们依然是参考quickstart项目中的类来实现,首先咱们要经过shiro.ini建立一个SecurityManager,再将这个SecurityManager设置为单例模式,以下:
Factory<org.apache.shiro.mgt.SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance(); SecurityUtils.setSecurityManager(securityManager);
如此以后,咱们就配置好了一个基本的Shiro环境,注意此时的用户和角色信息咱们配置在shiro.ini这个配置文件中,接下来咱们就能够获取一个Subject了,这个Subject就是咱们当前的用户对象,获取方式以下:
Subject currentUser = SecurityUtils.getSubject();
拿到这个用户对象以后,接下来咱们能够获取一个session了,这个session和咱们web中的HttpSession的操做基本上是一致的,不一样的是,这个session不依赖任何容器,能够随时随地获取,获取和操做方式以下:
//获取session Session session = currentUser.getSession(); //给session设置属性值 session.setAttribute("someKey", "aValue"); //获取session中的属性值 String value = (String) session.getAttribute("someKey");
说了这么多,咱们的用户到如今尚未登陆呢,Subject中有一个isAuthenticated方法用来判断当前用户是否已经登陆,若是isAuthenticated方法返回一个false,则表示当前用户未登陆,那咱们就能够执行登录,登陆方式以下:
if (!currentUser.isAuthenticated()) { UsernamePasswordToken token = new UsernamePasswordToken("sang", "123"); try { currentUser.login(token); } catch (UnknownAccountException uae) { log.info("There is no user with username of " + token.getPrincipal()); } catch (IncorrectCredentialsException ice) { log.info("Password for account " + token.getPrincipal() + " was incorrect!"); } catch (LockedAccountException lae) { log.info("The account for username " + token.getPrincipal() + " is locked. " + "Please contact your administrator to unlock it."); } catch (AuthenticationException ae) { } }
首先构造UsernamePasswordToken,两个参数就是咱们的用户名和密码,而后调用Subject中的login方法执行登陆,当用户名输错,密码输错、或者帐户锁定等问题出现时,系统会经过抛异常告知调用者这些问题。
当登陆成功以后,咱们能够经过以下方式获取当前登录用户的用户名:
log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");
咱们也能够经过调用Subject中的hasRole和isPermitted方法来判断当前用户是否具有某种角色或者某种权限,以下:
if (currentUser.hasRole("admin")) { log.info("May the Schwartz be with you!"); } else { log.info("Hello, mere mortal."); } if (currentUser.isPermitted("lightsaber:wield")) { log.info("You may use a lightsaber ring. Use it wisely."); } else { log.info("Sorry, lightsaber rings are for schwartz masters only."); }
最后,咱们能够经过logout方法注销本次登陆,以下:
currentUser.logout();
OK,至此,咱们经过官方案例给小伙伴们简单介绍了Shiro中的登陆操做,完整案例你们能够参考官方的demo。
首先咱们来看shiro官方文档中这样一张登陆流程图:
参照此图,咱们的登陆一共要通过以下几个步骤:
OK,经过上面的介绍,相信小伙伴们对整个登陆流程都有必定的理解了,小伙伴能够经过打断点来验证咱们上文所说的五个步骤。那么在上面的五个步骤中,小伙伴们看到了有一个Realm承担了很重要的一部分工做,那么这个Realm究竟是个什么东西,接下来咱们就来仔细看一看。
根据Realm文档上的解释,Realms担当Shiro和你的应用程序的安全数据之间的“桥梁”或“链接器”。当它实际上与安全相关的数据如用来执行身份验证(登陆)及受权(访问控制)的用户账户交互时,Shiro从一个或多个为应用程序配置的Realm 中寻找许多这样的东西。在这个意义上说,Realm 本质上是一个特定安全的DAO:它封装了数据源的链接详细信息,使Shiro 所需的相关的数据可用。当配置Shiro 时,你必须指定至少一个Realm 用来进行身份验证和/或受权。SecurityManager可能配置多个Realms,但至少有一个是必须的。Shiro 提供了当即可用的Realms 来链接一些安全数据源(即目录),如LDAP,关系数据库(JDBC),文本配置源,像INI 及属性文件,以及更多。你能够插入你本身的Realm 实现来表明自定义的数据源,若是默认地Realm不符合你的需求。
看了上面这一段解释,可能还有小伙伴云里雾里,那么接下来咱们来经过一个简单的案例来看看Realm到底扮演了一个什么样的做用,注意,本文的案例在上文案例的基础上完成。首先自定义一个MyRealm,内容以下:
public class MyRealm implements Realm { public String getName() { return "MyRealm"; } public boolean supports(AuthenticationToken token) { return token instanceof UsernamePasswordToken; } public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String password = new String(((char[]) token.getCredentials())); String username = token.getPrincipal().toString(); if (!"sang".equals(username)) { throw new UnknownAccountException("用户不存在"); } if (!"123".equals(password)) { throw new IncorrectCredentialsException("密码不正确"); } return new SimpleAuthenticationInfo(username, password, getName()); } }
自定义Realm实现Realm接口,该接口中有三个方法,第一个getName方法用来获取当前Realm的名字,第二个supports方法用来判断这个realm所支持的token,这里我假设值只支持UsernamePasswordToken类型的token,第三个getAuthenticationInfo方法则进行了登录逻辑判断,从token中取出用户的用户名密码等,进行判断,固然,我这里省略掉了数据库操做,当登陆验证出现问题时,抛异常便可,这里抛出的异常,将在执行登陆那里捕获到(注意,因为我这里定义的MyRealm是实现了Realm接口,因此这里的用户名和密码都须要我手动判断是否正确,后面的文章我会介绍其余写法)。
OK,建立好了MyRealm以后还不够,咱们还须要作一个简单配置,让MyRealm生效,将shiro.ini文件中的全部东西都注释掉,添加以下两行:
MyRealm= org.sang.MyRealm securityManager.realms=$MyRealm
第一行表示定义了一个realm,第二行将这个定义好的交给securityManger,这里实际上会调用到RealmSecurityManager类的setRealms方法。OK,作好这些以后,小伙伴们能够在MyRealm类中的一些关键节点打上断点,再次执行main方法,看看整个的登陆流程。
经过查看类的继承关系,咱们发现Realm的子类实际上有不少种,这里咱们就来看看有表明性的几种:
可能咱们并不知道,实际上这个类在咱们第二篇文章中就已经用过了。这个类一开始就有以下两行定义:
public static final String USERS_SECTION_NAME = "users"; public static final String ROLES_SECTION_NAME = "roles";
这两行配置表示shiro.ini文件中,[users]下面的表示表用户名密码还有角色,[roles]下面的则是角色和权限的对应关系。
PropertiesRealm则规定了另一种用户、角色定义方式,以下:
user.user1=password,role1
role.role1=permission1
这个顾名思义,就是从数据库中查询用户的角色、权限等信息。打开JdbcRealm类,咱们看到源码中有以下几行:
protected static final String DEFAULT_AUTHENTICATION_QUERY = "select password from users where username = ?"; protected static final String DEFAULT_SALTED_AUTHENTICATION_QUERY = "select password, password_salt from users where username = ?"; protected static final String DEFAULT_USER_ROLES_QUERY = "select role_name from user_roles where username = ?"; protected static final String DEFAULT_PERMISSIONS_QUERY = "select permission from roles_permissions where role_name = ?";
根据这几行预设的SQL咱们就能够大体推断出数据库中表的名称以及字段了,固然,咱们也能够自定义SQL。JdbcRealm其实是AuthenticatingRealm的子类,关于AuthenticatingRealm咱们在后面还会详细说到,这里先不展开。接下来咱们就来详细说说这个JdbcRealm。
使用JdbcRealm,涉及到数据库操做,要用到数据库链接池,这里我使用Druid数据库链接池,所以首先添加以下依赖:
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>RELEASE</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.27</version> </dependency>
想要使用JdbcRealm,那我首先要建立数据库,根据JdbcRealm中预设的SQL,我定义的数据库表结构以下:
这里为了你们可以直观的看到表的关系,我使用了外键,实际工做中,视状况而定。而后向表中添加几条测试数据。数据库脚本小伙伴能够在github上下载到(https://github.com/lenve/shir...
而后将shiro.ini中的全部配置注释掉,添加以下配置:
jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm dataSource=com.alibaba.druid.pool.DruidDataSource dataSource.driverClassName=com.mysql.jdbc.Driver dataSource.url=jdbc:mysql://localhost:3306/shiroDemo dataSource.username=root dataSource.password=123 jdbcRealm.dataSource=$dataSource jdbcRealm.permissionsLookupEnabled=true securityManager.realms=$jdbcRealm
这里的配置文件都很简单,不作过多赘述,小伙伴惟一须要注意的是permissionsLookupEnabled须要设置为true,不然一会JdbcRealm就不会去查询权限用户权限。
OK,作完上面几步就能够测试了,测试方式和第二篇文章中同样,咱们能够测试下用户登陆,用户角色和用户权限。
小伙伴们看懂了上文,对于自定义查询SQL就没什么问题了。我这里举一个简单的例子,好比我要自定义authenticationQuery对对应的SQL,查看JdbcRealm源码,咱们发现authenticationQuery对应的SQL原本是select password from users where username = ?
,若是须要修改的话,好比说个人表名不是users而是employee,那么在shiro.ini中添加以下配置便可:
jdbcRealm.authenticationQuery=select password from employee where username = ?
OK,这个小伙伴下来本身作尝试,我这里就不演示了。
不知道小伙伴们是否还记得这张登陆流程图:
从这张图中咱们能够清晰看到Realm是能够有多个的,不过到目前为止,咱们全部的案例都仍是单Realm,那么咱们先来看一个简单的多Realm状况。
前面的文章咱们本身建立了一个MyRealm,也用过JdbcRealm,但都是单独使用的,如今我想将两个一块儿使用,只须要修改shiro.ini配置便可,以下:
MyRealm= org.sang.MyRealm jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm dataSource=com.alibaba.druid.pool.DruidDataSource dataSource.driverClassName=com.mysql.jdbc.Driver dataSource.url=jdbc:mysql://localhost:3306/shiroDemo dataSource.username=root dataSource.password=123 jdbcRealm.dataSource=$dataSource jdbcRealm.permissionsLookupEnabled=true securityManager.realms=$jdbcRealm,$MyRealm
可是此时我数据库中用户的信息是sang/123,MyRealm中配置的信息也是sang/123,我把MyRealm中的用户信息修改成江南一点雨/456
,此时,个人MyRealm的getAuthenticationInfo方法以下:
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String password = new String(((char[]) token.getCredentials())); String username = token.getPrincipal().toString(); if (!"江南一点雨".equals(username)) { throw new UnknownAccountException("用户不存在"); } if (!"456".equals(password)) { throw new IncorrectCredentialsException("密码不正确"); } return new SimpleAuthenticationInfo(username, password, getName()); }
这个时候咱们就配置了两个Realm,仍是使用咱们一开始的测试代码进行登陆测试,这个时候咱们发现我既可使用江南一点雨/456
进行登陆,也可使用sang/123
进行登陆,用sang/123
登陆成功以后用户的角色信息和以前是同样的,而用江南一点雨/456
登陆成功以后用户没有角色,这个也很好理解,由于咱们在MyRealm中没有给用户配置任何权限。总而言之,就是当我有了两个Realm以后,如今只须要这两个Realm中的任意一个认证成功,就算我当前用户认证成功。
好了,有了上面的问题后,接下来咱们在Subject的login方法上打断点,跟随程序的执行步骤,咱们来到了ModularRealmAuthenticator类的doMultiRealmAuthentication方法中,以下:
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { this.assertRealmsConfigured(); Collection<Realm> realms = this.getRealms(); return realms.size() == 1?this.doSingleRealmAuthentication((Realm)realms.iterator().next(), authenticationToken):this.doMultiRealmAuthentication(realms, authenticationToken); }
在这个方法中,首先会获取当前一共有多少个realm,若是只有一个则执行doSingleRealmAuthentication方法进行处理,若是有多个realm,则执行doMultiRealmAuthentication方法进行处理。doSingleRealmAuthentication方法部分源码以下:
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) { ... ... AuthenticationInfo info = realm.getAuthenticationInfo(token); if(info == null) { String msg = "Realm [" + realm + "] was unable to find account data for the submitted AuthenticationToken [" + token + "]."; throw new UnknownAccountException(msg); } else { return info; } }
小伙伴们看到这里就明白了,这里调用了realm的getAuthenticationInfo方法,这个方法实际上就是咱们本身实现的MyRealm中的getAuthenticationInfo方法。
那若是有多个Realm呢?咱们来看看doMultiRealmAuthentication方法的实现,部分源码以下:
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) { AuthenticationStrategy strategy = this.getAuthenticationStrategy(); AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token); Iterator var5 = realms.iterator(); while(var5.hasNext()) { Realm realm = (Realm)var5.next(); aggregate = strategy.beforeAttempt(realm, token, aggregate); if(realm.supports(token)) { AuthenticationInfo info = null; Throwable t = null; try { info = realm.getAuthenticationInfo(token); } catch (Throwable var11) { } aggregate = strategy.afterAttempt(realm, token, info, aggregate, t); } else { log.debug("Realm [{}] does not support token {}. Skipping realm.", realm, token); } } aggregate = strategy.afterAllAttempts(token, aggregate); return aggregate; }
我这里主要来讲下这个方法的实现思路:
OK,通过上面的简单解析,小伙伴们对认证策略应该有一个大体的认识了,那么在Shiro中,一共支持三种不一样的认证策略,以下:
配置方式也很简单,在shiro.ini中进行配置,在上面配置的基础上,增长以下配置:
authenticator=org.apache.shiro.authc.pam.ModularRealmAuthenticator securityManager.authenticator=$authenticator allSuccessfulStrategy=org.apache.shiro.authc.pam.AllSuccessfulStrategy securityManager.authenticator.authenticationStrategy=$allSuccessfulStrategy
此时,咱们再进行登陆测试,则会要求每一个Realm都认证经过才算认证经过。
2011年12月21日,有人在网络上公开了一个包含600万个CSDN用户资料的数据库,数据所有为明文储存,包含用户名、密码以及注册邮箱。事件发生后CSDN在微博、官方网站等渠道发出了声明,解释说此数据库系2009年备份所用,因不明缘由泄露,已经向警方报案。后又在官网网站发出了公开道歉信。在接下来的十多天里,金山、网易、京东、当当、新浪等多家公司被卷入到此次事件中。整个事件中最触目惊心的莫过于CSDN把用户密码明文存储,因为不少用户是多个网站共用一个密码,所以一个网站密码泄露就会形成很大的安全隐患。因为有了这么多前车可鉴,咱们如今作系统时,密码都要加密处理。
密码加密咱们通常会用到散列函数,又称散列算法、哈希函数,是一种从任何一种数据中建立小的数字“指纹”的方法。散列函数把消息或数据压缩成摘要,使得数据量变小,将数据的格式固定下来。该函数将数据打乱混合,从新建立一个叫作散列值的指纹。散列值一般用一个短的随机字母和数字组成的字符串来表明。好的散列函数在输入域中不多出现散列冲突。在散列表和数据处理中,不抑制冲突来区别数据,会使得数据库记录更难找到。咱们经常使用的散列函数有以下几种:
MD5消息摘要算法是一种被普遍使用的密码散列函数,能够产生出一个128位(16字节)的散列值,用于确保信息传输完整一致。MD5由美国密码学家罗纳德·李维斯特设计,于1992年公开,用以取代MD4算法。这套算法的程序在 RFC 1321中被加以规范。将数据(如一段文字)运算变为另外一固定长度值,是散列算法的基础原理。1996年后被证明存在弱点,能够被加以破解,对于须要高度安全性的数据,专家通常建议改用其余算法,如SHA-2。2004年,证明MD5算法没法防止碰撞,所以不适用于安全性认证,如SSL公开密钥认证或是数字签名等用途。
安全散列算法(Secure Hash Algorithm)是一个密码散列函数家族,是FIPS所认证的安全散列算法。能计算出一个数字消息所对应到的,长度固定的字符串(又称消息摘要)的算法。且若输入的消息不一样,它们对应到不一样字符串的机率很高。SHA家族的算法,由美国国家安全局所设计,并由美国国家标准与技术研究院发布,是美国的政府标准,其分别是:SHA-0:1993年发布,是SHA-1的前身;SHA-1:1995年发布,SHA-1在许多安全协议中广为使用,包括TLS和SSL、PGP、SSH、S/MIME和IPsec,曾被视为是MD5的后继者。但SHA-1的安全性在2000年之后已经不被大多数的加密场景所接受。2017年荷兰密码学研究小组CWI和Google正式宣布攻破了SHA-1;SHA-2:2001年发布,包括SHA-22四、SHA-25六、SHA-38四、SHA-5十二、SHA-512/22四、SHA-512/256。虽然至今还没有出现对SHA-2有效的攻击,它的算法跟SHA-1基本上仍然类似;所以有些人开始发展其余替代的散列算法;SHA-3:2015年正式发布,SHA-3并非要取代SHA-2,由于SHA-2目前并无出现明显的弱点。因为对MD5出现成功的破解,以及对SHA-0和SHA-1出现理论上破解的方法,NIST感受须要一个与以前算法不一样的,可替换的加密散列算法,也就是如今的SHA-3。
Shiro中对以上两种散列算法都提供了支持,对于MD5,Shiro中生成消息摘要的方式以下:
Md5Hash md5Hash = new Md5Hash("123", null, 1024);
第一个参数是要生成密码的明文,第二个参数密码的盐值,第三个参数是生成消息摘要的迭代次数。
Shiro中对于安全散列算法的支持以下(支持多种算法,这里我举一个例子):
Sha512Hash sha512Hash = new Sha512Hash("123", null, 1024);
这里三个参数含义与上文基本一致,再也不赘述。shiro中也提供了通用的算法,以下:
SimpleHash md5 = new SimpleHash("md5", "123", null, 1024); SimpleHash sha512 = new SimpleHash("sha-512", "123", null, 1024);
当用户注册时,咱们能够经过上面的方式对密码进行加密,将加密后的字符串存入数据库中。我这里为了简单,就不写注册功能了,就把昨天数据库中用户的密码123改为sha512所对应的字符串,以下:
cb5143cfcf5791478e057be9689d2360005b3aac951f947af1e6e71e3661bf95a7d14183dadfb0967bd6338eb4eb2689e9c227761e1640e6a033b8725fabc783
同时,为了不其余Realm的干扰,数据库中我只配置一个JdbcRealm。
此时若是我不作其余修改的话,登陆必然会失败,缘由很简单:我登陆时输入的密码是123,可是数据库中的密码是一个很长的字符串,因此登陆确定不会成功。经过打断点,咱们发现最终的密码比对是在SimpleCredentialsMatcher类中的doCredentialsMatch方法中进行密码比对的,比对的方式也很简单,直接使用了对用户输入的密码和数据库中的密码生成byte数组而后进行比较,最终的比较在MessageDigest类的isEqual方法中。部分逻辑以下:
protected boolean equals(Object tokenCredentials, Object accountCredentials) { ... ... //获取用户输入密码的byte数组 byte[] tokenBytes = this.toBytes(tokenCredentials); //获取数据库中密码的byte数组 byte[] accountBytes = this.toBytes(accountCredentials); return MessageDigest.isEqual(tokenBytes, accountBytes); ... }
MessageDigest的isEqual方法以下:
public static boolean isEqual(byte[] digesta, byte[] digestb) { if (digesta == digestb) return true; if (digesta == null || digestb == null) { return false; } if (digesta.length != digestb.length) { return false; } int result = 0; // time-constant comparison for (int i = 0; i < digesta.length; i++) { result |= digesta[i] ^ digestb[i]; } return result == 0; }
都是很容易理解的比较代码,这里不赘述。咱们如今之因此登陆失败是由于没有对用户输入的密码进行加密,经过对源代码的分析,咱们发现是由于在AuthenticatingRealm类的assertCredentialsMatch方法中获取了一个名为SimpleCredentialsMatcher的密码比对器,这个密码比对器中比对的方法就是简单的比较,所以若是咱们可以将这个密码比对器换掉就行了。咱们来看一下CredentialsMatcher的继承关系:
咱们发现这个恰好有一个Sha512CredentialsMatcher比对器,这个比对器的doCredentialsMatch方法在它的父类HashedCredentialsMatcher,方法内容以下:
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { Object tokenHashedCredentials = hashProvidedCredentials(token, info); Object accountCredentials = getCredentials(info); return equals(tokenHashedCredentials, accountCredentials); }
这时咱们发现获取tokenHashedCredentials的方式不像之前那样简单粗暴了,而是调用了hashProvidedCredentials方法,而hashProvidedCredentials方法最终会来到下面这个重载方法中:
protected Hash hashProvidedCredentials(Object credentials, Object salt, int hashIterations) { String hashAlgorithmName = assertHashAlgorithmName(); return new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations); }
这几行代码似曾相识,很明显,是系统帮咱们对用户输入的密码进行了转换。了解了这些以后,那我只须要将shiro.ini修改为以下样子便可实现登陆了:
sha512=org.apache.shiro.authc.credential.Sha512CredentialsMatcher # 迭代次数 sha512.hashIterations=1024 jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm dataSource=com.alibaba.druid.pool.DruidDataSource dataSource.driverClassName=com.mysql.jdbc.Driver dataSource.url=jdbc:mysql://localhost:3306/shiroDemo dataSource.username=root dataSource.password=123 jdbcRealm.dataSource=$dataSource jdbcRealm.permissionsLookupEnabled=true # 修改JdbcRealm中的credentialsMatcher属性 jdbcRealm.credentialsMatcher=$sha512 securityManager.realms=$jdbcRealm
如此以后,咱们再进行登陆测试,就能够登陆成功了。
本小节案例下载:https://github.com/lenve/shir...
无论是消息摘要算法仍是安全散列算法,若是原文同样,生成密文也是同样的,这样的话,若是两个用户的密码原文同样,存到数据库中密文也就同样了,仍是不安全,咱们须要作进一步处理,常看法决方案就是加盐。盐从那里来呢?咱们可使用用户id(由于通常状况下,用户id是惟一的),也可使用一个随机字符,我这里采用第一种方案。
shiro中加盐的方式很简单,在用户注册时生成密码密文时,就要加入盐,以下几种方式:
Md5Hash md5Hash = new Md5Hash("123", "sang", 1024); Sha512Hash sha512Hash = new Sha512Hash("123", "sang", 1024); SimpleHash md5 = new SimpleHash("md5", "123", "sang", 1024); SimpleHash sha512 = new SimpleHash("sha-512", "123", "sang", 1024)
而后咱们首先将sha512生成的字符串放入数据库中,接下来我要配置一下个人jdbcRealm,由于我要指定个人盐是什么。在这里个人盐就是个人用户名,每一个用户的用户名是不同的,所以这里无法写死,在JdbcRealm中,系统提供了四种不一样的SaltStyle,以下:
SaltStyle | 含义 |
---|---|
NO_SALT | 默认,密码不加盐 |
CRYPT | 密码是以Unix加密方式储存的 |
COLUMN | salt是单独的一列储存在数据库中 |
EXTERNAL | salt没有储存在数据库中,须要经过JdbcRealm.getSaltForUser(String)函数获取 |
四种不一样的SaltStyle对应了四种不一样的密码处理方式,部分源码以下:
switch (saltStyle) { case NO_SALT: password = getPasswordForUser(conn, username)[0]; break; case CRYPT: // TODO: separate password and hash from getPasswordForUser[0] throw new ConfigurationException("Not implemented yet"); //break; case COLUMN: String[] queryResults = getPasswordForUser(conn, username); password = queryResults[0]; salt = queryResults[1]; break; case EXTERNAL: password = getPasswordForUser(conn, username)[0]; salt = getSaltForUser(username); }
在COLUMN这种状况下,SQL查询结果应该包含两列,第一列是密码,第二列是盐,这里默认执行的SQL在JdbcRealm一开头就定义好了,以下:
protected static final String DEFAULT_SALTED_AUTHENTICATION_QUERY = "select password, password_salt from users where username = ?";
即系统默认的盐是数据表中的password_salt提供的,可是我这里是username字段提供的,因此这里我一会要自定义这条SQL。自定义方式很简单,修改shiro.ini文件,添加以下两行:
jdbcRealm.saltStyle=COLUMN jdbcRealm.authenticationQuery=select password,username from users where username=?
首先设置saltStyle为COLUMN,而后从新定义authenticationQuery对应的SQL。注意返回列的顺序很重要,不能随意调整。如此以后,系统就会自动把username字段做为盐了。
不过,因为ini文件中不支持枚举,saltStyle的值其实是一个枚举类型,因此咱们在测试的时候,须要增长一个枚举转换器在咱们的main方法中,以下:
BeanUtilsBean.getInstance().getConvertUtils().register(new AbstractConverter() { @Override protected String convertToString(Object value) throws Throwable { return ((Enum) value).name(); } @Override protected Object convertToType(Class type, Object value) throws Throwable { return Enum.valueOf(type, value.toString()); } @Override protected Class getDefaultType() { return null; } }, JdbcRealm.SaltStyle.class);
固然,之后当咱们将shiro和web项目整合以后,就不须要这个转换器了。
如此以后,咱们就能够再次进行登陆测试了,会发现没什么问题了。
OK,刚刚是在JdbcRealm中配置了盐,若是没用JdbcRealm,而是本身定义的普通Realm,要怎么解决配置盐的问题?
首先要说明一点是,咱们前面的文章在自定义Realm时都是经过实现Realm接口实现的,这种方式有一个缺陷,就是密码比对须要咱们本身完成,通常在项目中,咱们自定义Realm都是经过继承AuthenticatingRealm或者AuthorizingRealm,由于这两个方法中都重写了getAuthenticationInfo方法,而在getAuthenticationInfo方法中,调用doGetAuthenticationInfo方法获取登陆用户,获取到以后,会调用assertCredentialsMatch方法进行密码比对,而咱们直接实现Realm接口则没有这一步,部分源码以下:
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { AuthenticationInfo info = getCachedAuthenticationInfo(token); if (info == null) { //调用doGetAuthenticationInfo获取info,这个doGetAuthenticationInfo是咱们在自定义Realm中本身实现的 info = doGetAuthenticationInfo(token); log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info); if (token != null && info != null) { cacheAuthenticationInfoIfPossible(token, info); } } else { log.debug("Using cached authentication info [{}] to perform credentials matching.", info); } if (info != null) { //获取到info以后,进行密码比对 assertCredentialsMatch(token, info); } else { log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token); } return info; }
基于上面所述的缘由,这里我先继承AuthenticatingRealm,以下:
public class MyRealm extends AuthenticatingRealm { public String getName() { return "MyRealm"; } public boolean supports(AuthenticationToken token) { return token instanceof UsernamePasswordToken; } protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String username = token.getPrincipal().toString(); if (!"sang".equals(username)) { throw new UnknownAccountException("用户不存在"); } String dbPassword = "a593ccad1351a26cf6d91d5f0f24234c6a4da5cb63208fae56fda809732dcd519129acd74046a1f9c5992db8903f50ebf3c1091b3aaf67a05c82b7ee470d9e58"; return new SimpleAuthenticationInfo(username, dbPassword, ByteSource.Util.bytes(username), getName()); } }
关于这个类,我说以下几点:
上面的核心步骤是第三步,系统去自动比较密码输入是否正确,在比对的过程当中,须要首先对用户输入的密码进行加盐加密,既然加盐加密,就会涉及到credentialsMatcher,这里咱们要用的credentialsMatcher实际上和在JdbcRealm中用的credentialsMatcher同样,只须要在配置文件中增长以下一行便可:
MyRealm.credentialsMatcher=$sha512
sha512和咱们上文定义的一致,这里就再也不重复说了。
本小节案例下载:https://github.com/lenve/shir...
密码加密加盐小伙伴们应该没有问题了,可是前面几篇文章又给咱们带来了一个新的问题:咱们前面IniRealm、JdbcRealm以及自定义的MyRealm,其中前两个咱们都能实现用户认证以及受权,即既能管理用户登陆,又能管理用户角色,而咱们自定义的MyRealm,目前还只能实现登陆,不能实现受权,本文咱们就来看看自定义Realm如何实现受权。
上篇文章咱们没有实现自定义Realm的受权操做,可是这个并不影响咱们调用hasRole方法去获取用户的权限,我在上文测试代码上的currentUser.hasRole上面打断点,经过层层追踪,咱们发现最终来到了ModularRealmAuthorizer类的hasRole方法中,部分源码以下:
public boolean hasRole(PrincipalCollection principals, String roleIdentifier) { assertRealmsConfigured(); for (Realm realm : getRealms()) { if (!(realm instanceof Authorizer)) continue; if (((Authorizer) realm).hasRole(principals, roleIdentifier)) { return true; } } return false; }
咱们看到在这里会遍历全部的realm,若是这个realm是Authorizer的实例,则会进行进一步的受权操做,若是不是Authorizer的实例,则直接跳过,而咱们只有一个自定义的MyRealm继承自AuthenticatingRealm,很明显不是Authorizer的实例,因此这里必然返回false,受权失败,因此要解决受权问题,第一步,得先让咱们的MyRealm成为Authorizer的实例。
以下图是Authorizer的继承关系:
小伙伴们看到,在Authorizer的实现类中有一个AuthorizingRealm,打开这个类,咱们发现它的继承关系以下:
public abstract class AuthorizingRealm extends AuthenticatingRealm implements Authorizer, Initializable, PermissionResolverAware, RolePermissionResolverAware { ... }
咱们发现,这个AuthorizingRealm不只是Authorizer的实现类,同时也是咱们上文所用的AuthenticatingRealm的实现类,既然AuthorizingRealm同时是这两个类的实现类,那么我把MyRealm的继承关系由AuthenticatingRealm改成AuthorizingRealm,确定不会影响我上文的功能,修改以后的MyRealm以下(部分关键代码):
public class MyRealm extends AuthorizingRealm { protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String username = token.getPrincipal().toString(); if (!"sang".equals(username)) { throw new UnknownAccountException("用户不存在"); } String dbPassword = "a593ccad1351a26cf6d91d5f0f24234c6a4da5cb63208fae56fda809732dcd519129acd74046a1f9c5992db8903f50ebf3c1091b3aaf67a05c82b7ee470d9e58"; return new SimpleAuthenticationInfo(username, dbPassword, ByteSource.Util.bytes(username), getName()); } protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { Set<String> roles = new HashSet<String>(); if ("sang".equals(principals.getPrimaryPrincipal().toString())) { roles.add("普通用户"); } return new SimpleAuthorizationInfo(roles); } }
继承了AuthorizingRealm以后,须要咱们实现doGetAuthorizationInfo方法。在这个方法中,咱们配置用户的权限。这里我为了方便,直接添加了普通用户这个权限,实际上,这里应该根据用户名去数据库里查询权限,查询方式不赘述。
经过源码追踪,咱们发现最终受权会来到AuthorizingRealm类的以下两个方法中:
public boolean hasRole(PrincipalCollection principal, String roleIdentifier) { AuthorizationInfo info = getAuthorizationInfo(principal); return hasRole(roleIdentifier, info); } protected boolean hasRole(String roleIdentifier, AuthorizationInfo info) { return info != null && info.getRoles() != null && info.getRoles().contains(roleIdentifier); }
这两个方法的逻辑很简单,第一个方法中调用的getAuthorizationInfo方法会最终调用到咱们自定义的doGetAuthorizationInfo方法,第二个hasRole方法接收的两个参数,第一个是用户申请的角色,第二个是用户具有的角色集,一个简单的contains函数就判断出用户是否具有某个角色了。
可是这个时候,用户只有角色,没有权限,咱们能够对doGetAuthorizationInfo方法作进一步的完善,以下:
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { Set<String> roles = new HashSet<String>(); Set<String> permiss = new HashSet<String>(); if ("sang".equals(principals.getPrimaryPrincipal().toString())) { roles.add("普通用户"); permiss.add("book:update"); } SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles); info.setStringPermissions(permiss); return info; }
固然,正常状况下,权限也应当是从数据库中查询获得的,我这里简化下。
那么这个角色是怎么验证的呢?追踪源码咱们来到了AuthorizingRealm类的以下两个方法中:
public boolean isPermitted(PrincipalCollection principals, Permission permission) { AuthorizationInfo info = getAuthorizationInfo(principals); return isPermitted(permission, info); } //visibility changed from private to protected per SHIRO-332 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; }
第一个isPermitted方法中调用了getAuthorizationInfo方法,而getAuthorizationInfo方法最终会调用到咱们本身定义的doGetAuthorizationInfo方法,即获取到用户的角色权限信息,而后在第二个方法中进行遍历判断,查看是否具有相应的权限,第二个isPermitted方法的第一个参数就是用户要申请的权限。
本小节案例下载:https://github.com/lenve/shir...
Spring和SpringMVC环境的搭建,总体上来讲,仍是比较容易的,由于这个不是本文的重点,所以这里我不作详细介绍,小伙伴能够在文末下载源码查看Spring+SpringMVC环境的搭建。同时,因为MyBatis的整合相对要容易不少,这里为了下降项目复杂度,我也就先不引入MyBatis。
对于项目依赖,除了Spring、SpringMVC、Shiro相关的依赖,还须要加入Shiro和Spring整合的jar,以下:
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>RELEASE</version> </dependency>
搭建好Spring+SpringMVC环境以后,整合Shiro咱们主要配置两个地方:
<filter> <filter-name>shiroFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <init-param> <param-name>targetFilterLifecycle</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
这样以后,当DelegatingFilterProxy拦截到全部请求以后,都会委托给shiroFilter来处理,shiroFilter是咱们第二步在Spring容器中配置的一个实例。
在Spring容器中至少有两个Bean须要咱们配置,一个就是第一步中的shiroFilter,还有一个就是SecurityManager,完整配置以下:
<bean class="org.apache.shiro.web.mgt.DefaultWebSecurityManager" id="securityManager"> </bean> <bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" id="shiroFilter"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login.jsp"></property> <property name="successUrl" value="/success.jsp"/> <property name="unauthorizedUrl" value="/unauthorized.jsp"/> <property name="filterChainDefinitions"> <value> /**=authc </value> </property> </bean>
这是一个很是简单的配置,咱们在之后的文章中还会继续完善它,关于这个配置我说以下几点:
/**=authc
表示全部的页面都须要认证(登陆)以后才能访问。/a/b/*=anon /a/**=authc
假设个人路径是/a/b/c那么就会匹配到第一个过滤器anon,而不会匹配到authc,因此这里的顺序很重要。
OK,这些配置写完后,在webpap目录下建立对应的jsp文件,以下:
此时,启动项目去浏览器中访问,不管咱们访问什么地址,最后都会回到login.jsp页面,由于全部的页面(即便不存在的地址)都须要认证后才能够访问。
本小节案例:https://github.com/lenve/shir...
很明显,无论是那种登陆,都离不开数据库,这里数据库我采用咱们前面的数据库,这里不作赘述(文末能够下载数据库脚本),可是我这里须要首先配置JdbcRealm,在applicationContext.xml中首先配置数据源,以下:
<context:property-placeholder location="classpath:db.properties"/> <bean class="com.alibaba.druid.pool.DruidDataSource" id="dataSource"> <property name="username" value="${db.username}"/> <property name="password" value="${db.password}"/> <property name="url" value="${db.url}"/> </bean>
有了数据源以后,接下来配置JdbcRealm,以下:
<bean class="org.apache.shiro.realm.jdbc.JdbcRealm" id="jdbcRealm"> <property name="dataSource" ref="dataSource"/> <property name="credentialsMatcher"> <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"> <property name="hashAlgorithmName" value="sha-512"/> <property name="hashIterations" value="1024"/> </bean> </property> <property name="saltStyle" value="COLUMN"/> <property name="authenticationQuery" value="select password, username from users where username = ?"/> </bean>
JdbcRealm中这几个属性和咱们本系列第七篇文章基本是一致的,首先咱们配置了密码比对器为HashedCredentialsMatcher,相应的算法为sha512,密码加密迭代次数为1024次,而后咱们配置了密码的盐从数据表的列中来,username列就是咱们的盐,这些配置和前文都是一致的,不清楚的小伙伴能够参考咱们本系列第七篇文章。
自定义登陆逻辑比较简单,首先咱们把login.jsp页面进行简单改造:
<form action="/login" method="post"> <table> <tr> <td>用户名:</td> <td><input type="text" name="username"></td> </tr> <tr> <td>密码:</td> <td><input type="password" name="password"></td> </tr> <tr> <td colspan="2"><input type="submit" value="登陆"></td> </tr> </table> </form>
而后建立咱们的登陆处理Controller,以下:
@PostMapping("/login") public String login(String username, String password) { Subject currentUser = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(username, password); try { currentUser.login(token); return "success"; } catch (AuthenticationException e) { } return "login"; }
登陆成功咱们就去success页面,登陆失败就回到登陆页面。作完这两步以后,咱们还要修改shiroFilter中的filterChainDefinitions属性,要设置/login
接口能够匿名访问,以下:
<bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" id="shiroFilter"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login.jsp"></property> <property name="successUrl" value="/success.jsp"/> <property name="unauthorizedUrl" value="/unauthorized.jsp"/> <property name="filterChainDefinitions"> <value> /login=anon /**=authc </value> </property> </bean>
作完这些以后,就能够去login.jsp页面测试登陆了。
上面中方式是咱们本身写登陆逻辑,shiro也给咱们提供了两种不用本身写登陆逻辑的登陆方式,请继续往下看。
shiro中也提供了基于http协议的认证,固然,这种认证也得有数据库的辅助,数据配置和前文同样,咱们只须要修改一个配置便可,以下:
<bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" id="shiroFilter"> <property name="securityManager" ref="securityManager"/> <property name="filterChainDefinitions"> <value> /**=authcBasic </value> </property> </bean>
这个表示全部的页面都要通过基于http的认证。此时咱们打开任意一个页面,认证方式以下:
表单登陆和基于HTTP的登陆相似,都是不须要咱们本身写登陆逻辑的登陆,可是出错的逻辑仍是要稍微处理下,首先修改shiroFilter:
<bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" id="shiroFilter"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login"/> <property name="successUrl" value="/success.jsp"/> <property name="filterChainDefinitions"> <value> /**=authc </value> </property> </bean>
配置登陆页面,也配置登陆成功后的跳转页面,同时设置全部页面都要登陆后才能访问。
配置登陆页面请求,以下:
@RequestMapping("/login") public String login(HttpServletRequest req, Model model) { String shiroLoginFailure = (String) req.getAttribute("shiroLoginFailure"); if (UnknownAccountException.class.getName().equals(shiroLoginFailure)) { model.addAttribute("error", "帐户不存在!"); } if (IncorrectCredentialsException.class.getName().equals(shiroLoginFailure)) { model.addAttribute("error", "密码不正确!"); } return "login"; }
若是登陆失败,那么在request中会有一个shiroLoginFailure的属性中保存了登陆失败的异常类名,经过判断这个类名,咱们就能够知道是什么缘由致使了登陆失败。
OK,配置好这两步以后,就能够去登陆页面测试了。
注销登陆比较简单,就一个过滤器,按以下方式配置:
<property name="filterChainDefinitions"> <value> /logout=logout /**=authc </value> </property>
经过get请求访问/logout
便可注销登陆。
本小节有三个案例,下载地址以下:
本文的案例在上文的基础上完成,所以Realm这一块我依然采用JdbcRealm,相关的受权就没必要配置了。可是这里的数据库脚本有更新,小伙伴须要下载从新执行(https://github.com/lenve/shir...
先来介绍下目前数据库中用户的状况,数据库中有两个用户,sang具备admin的角色,同时具备book:*
和author:create
两个权限,lisi具备user的角色,同时具备user:info
和user:delete
两个权限。修改shiroFilter,以下:
<bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" id="shiroFilter"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login"/> <property name="successUrl" value="/success.jsp"/> <property name="unauthorizedUrl" value="/unauthorized.jsp"/> <property name="filterChainDefinitions"> <value> /admin.jsp=authc,roles[admin] /user.jsp=authc,roles[user] /logout=logout /**=authc </value> </property> </bean>
关于这里的配置,我说以下几点:
测试时咱们分别用sang/123和lisi/123进行登陆,登陆成功后分别访问user.jsp和admin.jsp就能看到效果。
上面的方式是配置角色,可是尚未配置权限,要配置权限,首先要在jdbcRealm中添加容许权限信息的查询:
<property name="permissionsLookupEnabled" value="true"/>
而后配置下shiroFilter:
<property name="filterChainDefinitions"> <value> /admin.jsp=authc,roles[admin] /user.jsp=authc,roles[user] /userinfo.jsp=authc,perms[user:info] /bookinfo.jsp=authc,perms[book:info] /logout=logout /**=authc </value> </property>
这里假设访问userinfo.jsp须要user:info权限,访问bookinfo.jsp须要book:info权限。
OK,作完这些以后就能够测试了,分别用sang/123和lisi/123进行登陆,登陆成功后分别访问bookinfo.jsp和userinfo.jsp就能够看到不一样效果了。
本小节案例下载:https://github.com/lenve/shir...
上篇文章中,咱们在success.jsp中写了不少像下面这种超连接:
<h1>登陆成功!</h1> <h3><a href="/logout">注销</a></h3> <h3><a href="/admin.jsp">admin.jsp</a></h3> <h3><a href="/user.jsp">user.jsp</a></h3> <h3><a href="/bookinfo.jsp">bookinfo.jsp</a></h3> <h3><a href="/userinfo.jsp">userinfo.jsp</a></h3>
可是对于不一样身份的用户,并非每个连接都是有效的,点击无效的连接会进入到未受权的页面,这样用户体验并很差,最好可以把不可达的连接自动隐藏起来,同时,我也但愿可以方便获取当前登陆用户的信息等,考虑到这些需求,咱们来聊聊shiro中的jsp标签。
shiro中的标签并很少,主要有以下几种:
shiro:guest标签只有在当前未登陆时显示里边的内容,以下:
<shiro:guest> 欢迎【游客】访问! </shiro:guest>
shiro:user是在用户登陆以后显示该标签中的内容,不管是经过正常的登陆仍是经过Remember Me登陆,以下:
<shiro:user> 欢迎【<shiro:principal/>】访问! </shiro:user>
shiro:principal用来获取当前登陆用户的信息,显示效果以下:
4.shiro:authenticated
和shiro:user相比,shiro:authenticated的范围变小,当用户认证成功且不是经过Remember Me认证成功,这个标签中的内容才会显示出来:
<shiro:authenticated> 用户【<shiro:principal/>】身份认证经过,不是经过Remember Me认证! </shiro:authenticated>
shiro:notAuthenticated也是在用户未认证的状况下显示内容,和shiro:guest不一样的是,对于经过Remember Me方式进行的认证,shiro:guest不会显示内容,而shiro:notAuthenticated会显示内容(由于此时并非游客,可是又确实未认证),以下:
<shiro:notAuthenticated> 用户未进行身份认证 </shiro:notAuthenticated>
当用户不具有某个角色时候,显示内容,以下:
<shiro:lacksRole name="admin"> 用户不具有admin角色 </shiro:lacksRole>
当用户不具有某个权限时显示内容:
<shiro:lacksPermission name="book:info"> 用户不具有book:info权限 </shiro:lacksPermission>
当用户具有某个角色时显示的内容:
<shiro:hasRole name="admin"> <h3><a href="/admin.jsp">admin.jsp</a></h3> </shiro:hasRole>
当用户具有多个角色中的某一个时显示的内容:
<shiro:hasAnyRoles name="user,aaa"> <h3><a href="/user.jsp">user.jsp</a></h3> </shiro:hasAnyRoles>
当用户具有某一个权限时显示的内容:
<shiro:hasPermission name="book:info"> <h3><a href="/bookinfo.jsp">bookinfo.jsp</a></h3> </shiro:hasPermission>
本小节案例下载:https://github.com/lenve/shir...
使用缓存,首先须要添加相关依赖,以下:
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.4.0</version> </dependency>
ehcache的配置文件主要参考官方的配置,在resources目录下建立ehcache.xml文件,内容以下:
<ehcache> <diskStore path="java.io.tmpdir/shiro-spring-sample"/> <defaultCache maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="false" diskPersistent="false" diskExpiryThreadIntervalSeconds="120" /> <cache name="shiro-activeSessionCache" maxElementsInMemory="10000" eternal="true" overflowToDisk="true" diskPersistent="true" diskExpiryThreadIntervalSeconds="600"/> <cache name="org.apache.shiro.realm.SimpleAccountRealm.authorization" maxElementsInMemory="100" eternal="false" timeToLiveSeconds="600" overflowToDisk="false"/> </ehcache>
这些都是ehcache缓存中常规的配置,含义我就不一一解释了,文末下载源码有注释。
接下来咱们只须要在applicationContext中简单配置下缓存便可,配置方式以下:
<bean class="org.apache.shiro.cache.ehcache.EhCacheManager" id="cacheManager"> <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/> </bean> <bean class="org.apache.shiro.web.mgt.DefaultWebSecurityManager" id="securityManager"> <property name="realm" ref="jdbcRealm"/> <property name="cacheManager" ref="cacheManager"/> </bean>
首先配置EhCacheManager类,指定缓存位置,而后在DefaultWebSecurityManager中引入cacheManager便可,如此以后,咱们的缓存就应用上了。
因为我这里使用了JdbcRealm,若是使用了自定义Realm那么能够经过打日志看是否使用了缓存,使用了JdbcRealm以后,咱们能够经过打断点来查看是否应用了缓存,好比我执行以下代码:
subject.checkRole("admin"); subject.checkPermission("book:info");
经过断点跟踪,发现最终会来到AuthorizingRealm的getAuthorizationInfo方法中,在该方法中,首先会去缓存中检查数据,若是缓存中有数据,则不会执行doGetAuthorizationInfo方法(数据库操做就在doGetAuthorizationInfo方法中进行),若是缓存中没有数据,则会执行doGetAuthorizationInfo方法,而且在执行成功后将数据保存到缓存中(前提是配置了缓存,cache不为null),此时咱们经过断点,发现执行了缓存而没有查询数据库中的数据,部分源码以下:
protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) { AuthorizationInfo info = null; Cache<Object, AuthorizationInfo> cache = getAvailableAuthorizationCache(); if (cache != null) { Object key = getAuthorizationCacheKey(principals); info = cache.get(key); } if (info == null) { info = doGetAuthorizationInfo(principals); if (info != null && cache != null) { Object key = getAuthorizationCacheKey(principals); cache.put(key, info); } } return info; }
OK,总体来讲shiro中的缓存配置仍是很是简单的。
That's all.
本小节案例下载地址:https://github.com/lenve/shir...
待续。。。