共计 37265 个字符,预计需要花费 94 分钟才能阅读完成。
和大家分享一个松哥原创的 Shiro 教程吧,还没写完,先整一部分,剩下的敬请期待。
1.Shiro 简介
Apache Shiro 是一个开源平安框架,提供身份验证、受权、密码学和会话治理。Shiro 框架具备直观、易用等个性,同时也能提供强壮的安全性,尽管它的性能不如 SpringSecurity 那么弱小,然而在一般的我的项目中也够用了。
1.1 由来
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)。
1.2 有哪些性能
Apache Shiro 是一个弱小而灵便的开源平安框架,它干净利落地解决身份认证,受权,企业会话治理和加密。Apache Shiro 的首要指标是易于应用和了解。平安有时候是很简单的,甚至是苦楚的,但它没有必要这样。框架应该尽可能覆盖简单的中央,露出一个洁净而直观的 API,来简化开发人员在应用程序平安上所破费的工夫。
以下是你能够用 Apache Shiro 所做的事件:
- 验证用户来核实他们的身份
- 对用户执行访问控制,如:判断用户是否被调配了一个确定的平安角色;判断用户是否被容许做某事
- 在任何环境下应用 Session API,即便没有 Web 容器
- 在身份验证,访问控制期间或在会话的生命周期,对事件作出反应
- 汇集一个或多个用户平安数据的数据源,并作为一个繁多的复合用户“视图”
- 单点登录(SSO)性能
-
为没有关联到登录的用户启用 ”Remember Me” 服务
等等
Apache Shiro 是一个领有许多性能的综合性的程序平安框架。上面的图表展现了 Shiro 的重点:
Shiro 中有四大基石——身份验证,受权,会话治理和加密。
- Authentication:有时也简称为“登录”,这是一个证实用户是谁的行为。
- Authorization:访问控制的过程,也就是决定“谁”去拜访“什么”。
- Session Management:治理用户特定的会话,即便在非 Web 或 EJB 应用程序。
- Cryptography:通过应用加密算法放弃数据安全同时易于应用。
除此之外,Shiro 也提供了额定的性能来解决在不同环境下所面临的平安问题,尤其是以下这些:
- Web Support:Shiro 的 web 反对的 API 可能轻松地帮忙爱护 Web 应用程序。
- Caching:缓存是 Apache Shiro 中的第一层公民,来确保安全操作疾速而又高效。
- Concurrency:Apache Shiro 利用它的并发个性来反对多线程应用程序。
- Testing:测试反对的存在来帮忙你编写单元测试和集成测试。
- “Run As”:一个容许用户假如为另一个用户身份(如果容许)的性能,有时候在治理脚本很有用。
- “Remember Me”:在会话中记住用户的身份,这样用户只须要在强制登录时候登录。
2. 从一个简略的案例开始身份认证
2.1 shiro 下载
要学习 shiro,咱们首先需要去 shiro 官网下载 shiro,官网地址地址 https://shiro.apache.org/,截 … 在 2017-2019 已经停更了两年,我一度认为认为这个我的项目 gg 了),本文将采纳这个版本。当然,shiro 咱们也能够从 github 上下载到源码。两个源码下载地址如下:
1.apache shiro
2.github-shiro
下面我次要是和小伙伴们介绍下源码的下载,并没有波及到 jar 包的下载,jar 包咱们到时候间接应用 maven 即可。
2.2 创立演示工程
这里咱们先不急着写代码,咱们先关上刚刚下载到的源码,源码中有一个 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。
3. 聊一聊 Shiro 中的 Realm
3.1 登录流程是什么样的
首先咱们来看 shiro 官网文档中这样一张登录流程图:
参照此图,咱们的登录一共要通过如下几个步骤:
- 利用程序代码调用 Subject.login 办法,传递创立好的蕴含终端用户的 Principals(身份)和 Credentials(凭证)的 AuthenticationToken 实例(即上文例子中的 UsernamePasswordToken)。
- Subject 实例,通常是 DelegatingSubject(或子类)委托应用程序的 SecurityManager 通过调用 securityManager.login(token)开始真正的验证工作(在 DelegatingSubject 类的 login 办法中打断点即可看到)。
- SubjectManager 作为一个根本的“保护伞”的组成部分,接管 token 以及简略地委托给外部的 Authenticator 实例通过调用 authenticator.authenticate(token)。这通常是一个 ModularRealmAuthenticator 实例,反对在身份验证中协调一个或多个 Realm 实例。ModularRealmAuthenticator 实质上为 Apache Shiro 提供了 PAM-style 范式(其中在 PAM 术语中每个 Realm 都是一个 ’module’)。
- 如果应用程序中配置了一个以上的 Realm,ModularRealmAuthenticator 实例将利用配置好的 AuthenticationStrategy 来启动 Multi-Realm 认证尝试。在 Realms 被身份验证调用之前,期间和当前,AuthenticationStrategy 被调用使其可能对每个 Realm 的后果作出反应。如果只有一个繁多的 Realm 被配置,它将被间接调用,因为没有必要为一个繁多 Realm 的利用应用 AuthenticationStrategy。
- 每个配置的 Realm 用来帮忙看它是否反对提交的 AuthenticationToken。如果反对,那么反对 Realm 的 getAuthenticationInfo 办法将会随同着提交的 token 被调用。
OK,通过下面的介绍,置信小伙伴们对整个登录流程都有肯定的了解了,小伙伴能够通过打断点来验证咱们上文所说的五个步骤。那么在下面的五个步骤中,小伙伴们看到了有一个 Realm 承当了很重要的一部分工作,那么这个 Realm 到底是个什么货色,接下来咱们就来认真看一看。
3.2 什么是 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 办法,看看整个的登录流程。
4. 再来聊一聊 Shiro 中的 Realm
4.1 Realm 的继承关系
通过查看类的继承关系,咱们发现 Realm 的子类实际上有很多种,这里咱们就来看看有代表性的几种:
- IniRealm
可能咱们并不知道,实际上这个类在咱们第二篇文章中就曾经用过了。这个类一开始就有如下两行定义:
public static final String USERS_SECTION_NAME = "users";
public static final String ROLES_SECTION_NAME = "roles";
这两行配置示意 shiro.ini 文件中,[users]上面的示意表用户名明码还有角色,[roles]上面的则是角色和权限的对应关系。
- PropertiesRealm
PropertiesRealm 则规定了另外一种用户、角色定义形式,如下:
user.user1=password,role1
role.role1=permission1
- JdbcRealm
这个顾名思义,就是从数据库中查问用户的角色、权限等信息。关上 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。
4.2 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
小伙伴们看懂了上文,对于自定义查问 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, 这个小伙伴下来本人做尝试,我这里就不演示了。
5. Shiro 中多 Realm 的认证策略问题
5.1 多 Realm 认证策略
不晓得小伙伴们是否还记得这张登录流程图:
从这张图中咱们能够清晰看到 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 中的任意一个认证胜利,就算我以后用户认证胜利。
5.2 原理追踪
好了,有了下面的问题后,接下来咱们在 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;
}
我这里次要来说下这个办法的实现思路:
- 首先获取多 Realm 认证策略
- 构建一个 AuthenticationInfo 用来寄存一会认证胜利之后返回的信息
- 遍历 Realm,调用每个 Realm 中的 getAuthenticationInfo 办法,看是否可能认证胜利
- 每次获取到 AuthenticationInfo 之后,都调用 afterAttempt 办法进行后果合并
- 遍历完所有的 Realm 之后,调用 afterAllAttempts 进行后果合并,这里次要判断下是否一个都没匹配上
5.3 自在配置认证策略
OK,通过下面的简略解析,小伙伴们对认证策略应该有一个大抵的意识了,那么在 Shiro 中,一共反对三种不同的认证策略,如下:
- AllSuccessfulStrategy,这个示意所有的 Realm 都认证胜利才算认证胜利
- AtLeastOneSuccessfulStrategy,这个示意只有有一个 Realm 认证胜利就算认证胜利,默认即此策略
- FirstSuccessfulStrategy,这个示意只有第一个 Realm 认证胜利,就算认证胜利
配置形式也很简略,在 shiro.ini 中进行配置,在下面配置的根底上,减少如下配置:
authenticator=org.apache.shiro.authc.pam.ModularRealmAuthenticator
securityManager.authenticator=$authenticator
allSuccessfulStrategy=org.apache.shiro.authc.pam.AllSuccessfulStrategy
securityManager.authenticator.authenticationStrategy=$allSuccessfulStrategy
此时,咱们再进行登录测试,则会要求每个 Realm 都认证通过才算认证通过。
6. Shiro 中明码加密
6.1 明码为什么要加密
2011 年 12 月 21 日,有人在网络上公开了一个蕴含 600 万个 CSDN 用户材料的数据库,数据全副为明文贮存,蕴含用户名、明码以及注册邮箱。事件产生后 CSDN 在微博、官方网站等渠道收回了申明,解释说此数据库系 2009 年备份所用,因不明起因泄露,曾经向警方报案。后又在官网网站收回了公开道歉信。在接下来的十多天里,金山、网易、京东、当当、新浪等多家公司被卷入到这次事件中。整个事件中最惊心动魄的莫过于 CSDN 把用户明码明文存储,因为很多用户是多个网站共用一个明码,因而一个网站明码泄露就会造成很大的安全隐患。因为有了这么多前事不忘; 后事之师,咱们当初做零碎时,明码都要加密解决。
明码加密咱们个别会用到散列函数,又称散列算法、哈希函数,是一种从任何一种数据中创立小的数字“指纹”的办法。散列函数把音讯或数据压缩成摘要,使得数据质变小,将数据的格局固定下来。该函数将数据打乱混合,从新创立一个叫做散列值的指纹。散列值通常用一个短的随机字母和数字组成的字符串来代表。好的散列函数在输出域中很少呈现散列抵触。在散列表和数据处理中,不克制抵触来区别数据,会使得数据库记录更难找到。咱们罕用的散列函数有如下几种:
- MD5 音讯摘要算法
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-224、SHA-256、SHA-384、SHA-512、SHA-512/224、SHA-512/256。尽管至今尚未呈现对 SHA- 2 无效的攻打,它的算法跟 SHA- 1 基本上依然类似;因而有些人开始倒退其余代替的散列算法;SHA-3:2015 年正式公布,SHA- 3 并不是要取代 SHA-2,因为 SHA- 2 目前并没有呈现显著的弱点。因为对 MD5 呈现胜利的破解,以及对 SHA- 0 和 SHA- 1 呈现实践上破解的办法,NIST 感觉须要一个与之前算法不同的,可替换的加密散列算法,也就是当初的 SHA-3。
6.2 Shiro 中如何加密
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…
7. Shiro 中明码加盐
7.1 明码为什么要加盐
不论是音讯摘要算法还是平安散列算法,如果原文一样,生成密文也是一样的,这样的话,如果两个用户的明码原文一样,存到数据库中密文也就一样了,还是不平安,咱们须要做进一步解决,常见解决方案就是加盐。盐从那里来呢?咱们能够应用用户 id(因为个别状况下,用户 id 是惟一的),也能够应用一个随机字符,我这里采纳第一种计划。
7.2 Shiro 中如何实现加盐
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 我的项目整合之后,就不须要这个转换器了。
如此之后,咱们就能够再次进行登录测试了,会发现没什么问题了。
7.3 非 JdbcRealm 如何配置盐
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());
}
}
对于这个类,我说如下几点:
- 用户名我这里还是手动判断了下,实际上这个中央要从数据库查问用户信息,如果查不到用户信息,则间接抛 UnknownAccountException
- 返回的 SimpleAuthenticationInfo 中,第二个参数是明码,失常状况下,这个明码是从数据库中查问进去的,我这里间接写死了
- 第三个参数是盐值,这样结构好 SimpleAuthenticationInfo 之后返回,shiro 会去判断用户输出的明码是否正确
下面的外围步骤是第三步,零碎去主动比拟明码输出是否正确,在比对的过程中,须要首先对用户输出的明码进行加盐加密,既然加盐加密,就会波及到 credentialsMatcher,这里咱们要用的 credentialsMatcher 实际上和在 JdbcRealm 中用的 credentialsMatcher 一样,只须要在配置文件中减少如下一行即可:
MyRealm.credentialsMatcher=$sha512
sha512 和咱们上文定义的统一,这里就不再反复说了。
本大节案例下载:https://github.com/lenve/shir…
8. Shiro 中自定义带角色和权限的 Realm
明码加密加盐小伙伴们应该没有问题了,然而后面几篇文章又给咱们带来了一个新的问题:咱们后面 IniRealm、JdbcRealm 以及自定义的 MyRealm,其中前两个咱们都能实现用户认证以及受权,即既能治理用户登录,又能治理用户角色,而咱们自定义的 MyRealm,目前还只能实现登录,不能实现受权,本文咱们就来看看自定义 Realm 如何实现受权。
8.1 问题追踪
上篇文章咱们没有实现自定义 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 的实例。
8.2 解决方案
如下图是 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…
9. Shiro 整合 Spring
9.1 Spring&SpringMVC 环境搭建
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>
9.2 整合 Shiro
搭建好 Spring+SpringMVC 环境之后,整合 Shiro 咱们次要配置两个中央:
- web.xml 中配置代理过滤器,如下:
<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 容器
在 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>
这是一个非常简单的配置,咱们在当前的文章中还会持续欠缺它,对于这个配置我说如下几点:
- 首先咱们须要配置一个 securityManager,到时候咱们的 realm 要配置在这里。
- 还要配置一个名为 shiroFilter 的 bean,这个名字要和 web.xml 中代理过滤器的名字统一。
- shiroFilter 中,loginUrl 示意登录页面地址。
- successUrl 示意登录胜利地址。
- unauthorizedUrl 示意受权失败地址。
- filterChainDefinitions 中配置的
/**=authc
示意所有的页面都须要认证 (登录) 之后能力拜访。 - authc 实际上是一个过滤器,这个咱们在后文还会再具体说到。
- 匹配符遵循 Ant 格调门路表达式, 这里能够配置多个,匹配程序从上往下匹配到了就不再匹配了。比方上面这个写法:
/a/b/*=anon
/a/**=authc
假如我的门路是 /a/b/ c 那么就会匹配到第一个过滤器 anon,而不会匹配到 authc,所以这里的程序很重要。
OK,这些配置写完后,在 webpap 目录下创立对应的 jsp 文件,如下:
此时,启动我的项目去浏览器中拜访,无论咱们拜访什么地址,最初都会回到 login.jsp 页面,因为所有的页面(即便不存在的地址)都须要认证后才能够拜访。
本大节案例:https://github.com/lenve/shir…
10. Shiro 解决登录的三种形式
10.1 筹备工作
很显著,不论是那种登录,都离不开数据库,这里数据库我采纳咱们后面的数据库,这里不做赘述(文末能够下载数据库脚本),然而我这里须要首先配置 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 列就是咱们的盐,这些配置和前文都是统一的,不分明的小伙伴能够参考咱们本系列第七篇文章。
10.2 自定义登录逻辑
自定义登录逻辑比较简单,首先咱们把 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 也给咱们提供了两种不必本人写登录逻辑的登录形式,请持续往下看。
10.3 基于 HTTP 的认证
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 的认证。此时咱们关上任意一个页面,认证形式如下:
10.4 表单登录
表单登录和基于 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,配置好这两步之后,就能够去登录页面测试了。
10.5 登记登录
登记登录比较简单,就一个过滤器,按如下形式配置:
<property name="filterChainDefinitions">
<value>
/logout=logout
/**=authc
</value>
</property>
通过 get 申请拜访 /logout
即可登记登录。
本大节有三个案例,下载地址如下:
- https://github.com/lenve/shir…
- https://github.com/lenve/shir…
- https://github.com/lenve/shir…
11. Shiro 中的受权问题
11.1 配置角色
本文的案例在上文的根底上实现,因而 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>
对于这里的配置,我说如下几点:
- unauthorizedUrl 示意受权失败时展现的页面
- filterChainDefinitions 中咱们配置了 admin.jsp 页面必须登录后能力拜访,同时登录的用户必须具备 admin 角色,user.jsp 也是必须登录后能力拜访,同时登录的用户必须具备 user 角色
11.2 测试
测试时咱们别离用 sang/123 和 lisi/123 进行登录,登录胜利后别离拜访 user.jsp 和 admin.jsp 就能看到成果。
11.3 配置权限
下面的形式是配置角色,然而还没有配置权限,要配置权限,首先要在 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…
12. Shiro 中的 JSP 标签
12.1 缘起
上篇文章中,咱们在 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 标签。
12.2 标签介绍
shiro 中的标签并不多,次要有如下几种:
- shiro:guest
shiro:guest 标签只有在以后未登录时显示里边的内容,如下:
<shiro:guest>
欢送【游客】拜访!
</shiro:guest>
- shiro:user
shiro:user 是在用户登录之后显示该标签中的内容,无论是通过失常的登录还是通过 Remember Me 登录,如下:
<shiro:user>
欢送【<shiro:principal/>】拜访!
</shiro:user>
- shiro:principal
shiro:principal 用来获取以后登录用户的信息,显示成果如下:
4.shiro:authenticated
和 shiro:user 相比,shiro:authenticated 的范畴变小,当用户认证胜利且不是通过 Remember Me 认证胜利,这个标签中的内容才会显示进去:
<shiro:authenticated>
用户【<shiro:principal/>】身份认证通过,不是通过 Remember Me 认证!
</shiro:authenticated>
- shiro:notAuthenticated
shiro:notAuthenticated 也是在用户未认证的状况下显示内容,和 shiro:guest 不同的是,对于通过 Remember Me 形式进行的认证,shiro:guest 不会显示内容,而 shiro:notAuthenticated 会显示内容(因为此时并不是游客,然而又的确未认证),如下:
<shiro:notAuthenticated>
用户未进行身份认证
</shiro:notAuthenticated>
- shiro:lacksRole
当用户不具备某个角色时候,显示内容,如下:
<shiro:lacksRole name="admin">
用户不具备 admin 角色
</shiro:lacksRole>
- shiro:lacksPermission
当用户不具备某个权限时显示内容:
<shiro:lacksPermission name="book:info">
用户不具备 book:info 权限
</shiro:lacksPermission>
- shiro:hasRole
当用户具备某个角色时显示的内容:
<shiro:hasRole name="admin">
<h3><a href="/admin.jsp">admin.jsp</a></h3>
</shiro:hasRole>
- shiro:hasAnyRoles
当用户具备多个角色中的某一个时显示的内容:
<shiro:hasAnyRoles name="user,aaa">
<h3><a href="/user.jsp">user.jsp</a></h3>
</shiro:hasAnyRoles>
- shiro:hasPermission
当用户具备某一个权限时显示的内容:
<shiro:hasPermission name="book:info">
<h3><a href="/bookinfo.jsp">bookinfo.jsp</a></h3>
</shiro:hasPermission>
本大节案例下载:https://github.com/lenve/shir…
13.Shiro 中的缓存机制
13.1 增加依赖
应用缓存,首先须要增加相干依赖,如下:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.0</version>
</dependency>
13.2 增加配置文件
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 缓存中惯例的配置,含意我就不一一解释了,文末下载源码有正文。
13.3 缓存配置
接下来咱们只须要在 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 即可,如此之后,咱们的缓存就利用上了。
13.4 测试
因为我这里应用了 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…
待续。。。