Apache Shiro,是一个不同寻常的Java安全框架,为保护应用(命令行应用、移动端应用、大型网络应用)提供了简单而强大的方法。
一、什么是Apache Shiro?
Apache Shiro(发音为“shee-roh”,日语“堡垒(Castle)”的意思)是一个强大易用的Java安全框架,提供了认证、授权、加密和会话管理功能,可为任何应用提供安全保障 - 从命令行应用、移动应用到大型网络及企业应用。
Shiro为解决应用安全的如下四要素提供了相应的API:
认证 - 用户身份识别,常被称为用户“登录”;
授权 - 访问控制;
密码加密 - 保护或隐藏数据防止被偷窥;
会话管理 - 用户相关的时间敏感的状态。
Shiro还支持一些辅助特性,如Web应用安全、单元测试和多线程,它们的存在强化了这四个要素。
二、Apache Shiro的历史
Shiro的前身叫JSecurity项目,始于2003年,2008年加入Apache软件基金会并成为Apache顶级项目。
2003年,对于Java应用开发人员而言,没有太多的通用安全替代方案 ,而Java认证/授权服务(或称为JAAS)有太多的缺点 - 尽管它的认证功能尚可忍受,但授权方面的功能比较粗糙,此外,JAAS跟虚拟机层面的安全问题关系非常紧密,如判断JVM中是否允许装入一个 类。作为应用开发者,应该更关心应用最终用户能做什么,而不是我的代码在JVM中能做什么。
当时更多的开发人员需要一个于容器无关的会话机制,在当时,可以使用HttpSessions,但它需要Web容器,或者是使用EJB2.x中的会话bean(需要EJB容器),这并不能满足用户需要的与容器无关、可用于任何环境中得会话。
另外就是加密问题,Java密码架构(Java Cryptography Architecture)让人难以理解,除非你是密码学专家,API里到处都是Checked Exception,用起来很麻烦。
三、Apache Shiro的特性:
易于使用- 易用性是这个项目的最终目标。
广泛性- 没有其他安全框架可以达到Apache Shiro宣称的广度,它可以为你的安全需求提供“一站式”服务。
灵活性- Apache Shiro可以工作在任何应用环境中。虽然它工作在Web、EJB和IoC环境中,但它并不依赖这些环境。Shiro既不强加任何规范,也无需过多依赖。
Web能力- Apache Shiro对Web应用的支持很神奇,允许你基于应用URL和Web协议(如REST)创建灵活的安全策略,同时还提供了一套控制页面输出的JSP标签库。
可插拔- Shiro干净的API和设计模式使它可以方便地与许多的其他框架和应用进行集成。你将看到Shiro可以与诸如Spring、Grails、Wicket、Tapestry、Mule、Apache Camel、Vaadin这类第三方框架无缝集成。
支持 - Apache Shiro是Apache软件基金会成员,这是一个公认为了社区利益最大化而行动的组织。项目开发和用户组都有随时愿意提供帮助的友善成员。像Katasoft这类商业公司,还可以给你提供需要的专业支持和服务。
四、Shiro中的核心概念:Subject,SecurityManager和Realms
Subject : 当前用户相关的安全操作
在考虑应用安全时,你最常问的问题可能是“当前用户是谁?”或“当前用户允许做某个操作吗?”。当我们写代码或设计用户界面时,问自己这些问题很平常:应 用通常都是基于用户故事构建的,并且你希望功能描述(和安全)是基于每个用户的。所以,对于我们而言,考虑应用安全的最自然方式就是基于当前用户。 Shiro的API用它的Subject概念从根本上体现了这种思考方式。
Subject一词是一个安全术语,其基本意思是“当前的操作用户”。称之为“用户”并不准确,因为“用户”一 词通常跟人相关。在安全领域,术语“Subject”可以是人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。但考虑到大多数目的和用途,你可以把它认为是Shiro的“用户”概念。在代码 的任何地方,你都能轻易的获得Shiro Subject,参见如下代码:
import org.apache.shiro.subject.Subject; import org.apache.shiro.SecurityUtils; ... 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(尽管不必是一个静态Singleton)。跟Shiro里的几乎所有组件一样,SecurityManager的缺省实现是POJO,而且可用POJO兼容的任何配置机制进行配置 - 普通的Java代码、Spring XML、YAML、.properties和.ini文件等。基本来讲,能够实例化类和调用JavaBean兼容方法的任何配置形式都可使用。
默认的配置方式是基于文本的INI配置。INI易于阅读、使用简单并且需要极少依赖。你还能看到,只要简单地理解对象导航,INI可被有效地用于配置像 SecurityManager那样简单的对象图。但由于当今Spring在项目中的广泛使用,下面简单的介绍Spring XML配置SecurityManager的方式:
1、配置SecurityManager:
其中shiroJDBCRealm是自定义的Realm下面将会对Realm做进一步的介绍并可查看该bean shiroJDBCRealm的具体代码。
2、整合Spring和Shiro
Realm: 与安全相关的DAO:Shiro与应用数据之间的“桥梁”或者“连接器”
Shiro的第三个也是最后一个概念是Realm。Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当Shiro与像用户帐户这类安全相关数据进行交互,执行认证(登录)和授权(访问控制)时,Shiro会从应用配置的Realm中查找很多内容。从这个意义上讲,Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。当配置Shiro时,你必须至少指定一个Realm,用于认证和(或)授权。配置多个Realm是可以的,但是至少需要一个。
Shiro内置了可以连接大量安全数据源(又名目录)的Realm,如LDAP、关系数据库(JDBC)、类似INI的文本配置资源以及属性文件等。如果缺省的Realm不能满足需求,你还可以插入代表自定义数据源的自己的Realm实现,如下面的代码便是上一步中提到的shiroJDBCRealm
@Component("shiroJDBCRealm") public class ShiroJDBCRealm extends AuthorizingRealm { @Resource private IAuthDao authDao; @Resource private IRoleDao roleDao; // 授权(访问控制) @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { SimpleUser simpleUser = (SimpleUser) principalCollection.fromRealm(getName()).iterator().next(); // find user by login name Long uid = simpleUser.getUid(); try { SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); for (Role role : roleDao.getRoleByUid(uid)) { info.addRole(role.getRole()); } return info; } catch (BaseException ex) { return null; } } // 验证(登陆) @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { if (BearerToken.class.isInstance(authenticationToken)) { //token from header BearerToken bearerToken = (BearerToken) authenticationToken; String token = bearerToken == null ? "" : bearerToken.getToken(); try { Long userUid = authDao.isAuthorized(token); return new SimpleAuthenticationInfo(new SimpleUser(userUid, token), token, getName()); } catch (BaseException ex) { return null; } } else { // Just used to test, useless now. UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; String user = token.getUsername(); System.out.print(user); if ("wo".equals(user)) { return new SimpleAuthenticationInfo(user, token.getPassword(), getName()); } else { return null; } } } @Override public void onLogout(PrincipalCollection principals) { super.onLogout(principals); } }
下面将讨论作为一个开发人员,将如何使用Shiro
1、认证
认证是核实用户身份的过程。也就是说,当用户使用应用进行认证时,他们就在证明他们就是自己所说的那个人。有时这也理解为“登录”。它是一个典型的三步骤过程。
1)收集用户的身份信息,称为当事人(principal),以及身份的支持证明,称为证书(Credential)- 身份证。
2)将当事人和证书提交给系统。
3)如果提交的证书与系统期望的该用户身份(当事人)匹配,该用户就被认为是经过认证的,反之则被认为未经认证的。
这个过程的常见例子是大家都熟悉的“用户/密码”组合。多数用户在登录软件系统时,通常提供自己的用户名(当事人)和支持他们的密码(证书)。如果存储在系统中的密码(或密码表示)与用户提供的匹配,他们就被认为通过认证。
Shiro以简单直观的方式支持同样的流程。正如我们前面所说,Shiro有一个以Subject为中心的API - 几乎你想要用Shiro在运行时完成的所有事情都能通过与当前执行的Subject进行交互而达成。因此,要登录Subject,只需要简单地调用它的login方法,传入表示被提交当事人和证书(在这种情况下,就是用户名和密码)的AuthenticationToken实例。示例如清单5中所示:
Subject登录
//1. 接受提交的当事人和证书:
AuthenticationToken token = new UsernamePasswordToken(username, password); // Principal, Crendential
// 2. 获取当前Subject:
Subject currentUser = SecurityUtils.getSubject();
// 3. 登录:
currentUser.login(token);
你可以看到,Shiro的API很容易地就反映了这个常见流程。你将会在所有的Subject操作中继续看到这种简单风格。在调用了login方法后,SecurityManager会收到AuthenticationToken,并将其发送给已配置的Realm,执行必须的认证检查(doGetAuthenticationInfo)。每个Realm都能在必要时对提交的AuthenticationTokens作出反应。但是如果登录失败了会发生什么?如果用户提供了错误密码又会发生什么?通过对Shiro的运行时AuthenticationException做出相应的操作:
// 3. 登录:
try {
currentUser.login(token);
} catch (IncorrectCredentialsException ice) {
…
} catch (LockedAccountException lae) {
…
}
…
catch (AuthenticationException ae) {…
}
你可以选择捕获AuthenticationException的一个子类,作出特定的响应,或者对任何AuthenticationException做一般性处理(例如,显示给用户普通的“错误的用户名或密码”这类消息)。
Subject登录成功后,他们就被认为是已认证的,通常你会允许他们使用你的应用。但是仅仅证明了一个用户的身份并不意味着他们可以对你的应用为所欲为。这就引出了另一个问题,“我如何控制用户能做或不能做哪些事情?”,决定用户允许做哪些事情的过程被称为授权。下面我们将谈谈Shiro如何进行授权。
2、授权
授权实质上就是访问控制 - 控制用户能够访问应用中的哪些内容,比如资源、Web页面等等。多数用户执行访问控制是通过使用诸如角色和权限这类概念完成的。也就是说,通常用户允许或 不允许做的事情是根据分配给他们的角色或权限决定的。那么,通过检查这些角色和权限,你的应用程序就可以控制哪些功能是可以暴露的。如你期望的,Subject API让你可以很容易的执行角色和权限检查。如下代码片段所示:如何检查Subject被分配了某个角色:
角色检查
if (subject.hasRole(“administrator”) ) {
} else {
}
如代码所示,应用程序可基于访问控制检查打开或关闭某些功能。
权限检查是执行授权的另一种方法。上例中的角色检查有个很大的缺陷:你无法在运行时增删角色。角色名字在这里是硬编码,所以,如果你修改了角色名字或配置,你的代码就会乱套!如果你需要在运行时改变角色含义,或想要增删角色,就不能使用如上的方法。
为此,Shiro支持了权限(permissions)概念。权限是功能的原始表述,如‘开门’,‘创建一个博文’,‘删除‘jsmith’用户’等。通过让权限反映应用的原始功能,在改变应用功能时,你只需要改变权限检查。进而,你可以在运行时按需将权限分配给角色或用户。
如下代码重写了之前的用户检查,取而代之使用权限检查。
权限检查: Shiro的亮点之处
if (subject.isPermitted(“user:create”) ) {
//显示‘Create User’按钮
} else {
//按钮置灰?
}
这样,任何具有“user:create”权限的角色或用户都可以点击‘Create User’按钮,并且这些角色和指派甚至可以在运行时改变,从而提供了一个非常灵活的安全模型。
备注: “user:create”字符串是一个权限字符串的例子,它遵循特定的解析惯例。Shiro借助它的WildcardPermission支持这种开箱即用的惯例。WildcardPermission的方式非常灵活,甚至支持像实例级别访问控制这样的功能。
实例级别的权限检查
if (subject.isPermitted(“user:delete:jsmith”) ) {
//删除‘jsmith’用户
} else {
//不删除‘jsmith’
}
该例表明,你可以对你需要的单个资源进行访问控制,甚至深入到非常细粒度的实例级别。如果愿意,你甚至还可以发明自己的权限语法。参见Shiro Permission文档可以了解更多内容。最后,就像使用认证那样,上述调用最终会转向SecurityManager,它会咨询Realm做出自己的访问控制决定。必要时,还允许单个Realm同时响应认证和授权操作。
以上就是对Shiro授权功能的简要概述。虽然多数安全框架止于授权和认证,但Shiro提供了更多功能。下面,我们将谈谈Shiro的高级会话管理功能。
3、会话管理
在安全框架领域,Apache Shiro提供了一些独特的东西:可在任何应用或架构层一致地使用Session API。即Shiro为任何应用提供了一个会话编程范式 - 从小型后台独立应用到大型集群Web应用。这意味着,那些希望使用会话的应用开发者,不必被迫使用Servlet或EJB容器了。或者,如果正在使用这些容器,开发者现在也可以选择使用在任何层统一一致的会话API,从而取代Servlet或EJB机制。
Shiro会话最重要的一个好处或许就是它们是独立于容器的,例如,对集群会话来讲,支持容错和故障转移有多少种容器特定的方式?Tomcat的方式与Jetty的不同,而Jetty又和Websphere不一样,等等。但通过Shiro会话,你可以获得一个容器无关的集群解决方案。Shiro的架构允许可插拔的会话数据存储,如企业缓存、关系数据库、NoSQL等。这意味着,只要配置会话集群一次,它就会以相同的方式工作,跟部署环境无关(即对于基于Tomcat、Jetty、JEE服务器的应用或者是独立应用,不管如何部署应用,毋须重新配置应用)。
Shiro会话的另一好处就是,如果需要,会话数据可以跨客户端技术进行共享。例如,Swing桌面客户端在需要时可以参与到相同的Web应用会话中 - 如果用户同时使用这两种应用,这样的功能会很有用。如何在任何环境中访问Subject的会话呢?可通过如下的两个方法:
Subject的会话
Session session = subject.getSession();
Session session = subject.getSession(boolean create);
如上例子的这些方法在概念上等同于HttpServletRequest API。第一个方法会返回Subject的现有会话,或者如果还没有会话,它会创建一个新的并将之返回。第二个方法接受一个布尔参数,这个参数用于判定会话不存在时是否创建新会话。一旦获得Shiro的会话,你几乎可以像使用HttpSession一样使用它。当然,最大的不同在于,你可以在任何应用中使用Shiro会话,而不仅限于Web应用。
会话的方法
Session session = subject.getSession();
session.getAttribute("key", someValue);
Date start = session.getStartTimestamp();
Date timestamp = session.getLastAccessTime();
session.setTimeout(millis); ...
4、加密
加密是隐藏或混淆数据以避免被偷窥的过程。在加密方面,Shiro的目标是简化并让JDK的加密支持可用。
清楚一点很重要,一般情况下,加密不是特定于Subject的,所以它是Shiro API的一部分,但并不特定于Subject。你可以在任何地方使用Shiro的加密支持,甚至在不使用Subject的情况下。对于加密支 持,Shiro真正关注的两个领域是加密哈希(又名消息摘要)和加密密码。下面我们来看看这两个方面的详细描述。
哈希
如果你曾使用过JDK的MessageDigest类, 你会立刻意识到它的使用有点麻烦。MessageDigest类有一个笨拙的基于工厂的静态方法API,它不是面向对象的,并且你被迫去捕获那些永远都不 必捕获的Checked Exceptions。如果需要输出十六进制编码或Base64编码的消息摘要,你只有靠自己 - 对上述两种编码,没有标准的JDK支持它们。Shiro用一种干净而直观的哈希API解决了上述问题。
打个比方,考虑比较常见的情况,使用MD5哈希一个文件,并确定该哈希的十六进制值。被称为‘校验和’,这在提供文件下载时常用到 - 用户可以对下载文件执行自己的MD5哈希。如果它们匹配,用户完全可以认定文件在传输过程中没有被篡改。
不使用Shiro,你需要如下步骤才能完成上述内容:
将文件转换成字节数组。JDK中没有干这事的,故而你需要创建一个辅助方法用于打开FileInputStream,使用字节缓存区,并抛出相关的IOExceptions,等等。
使用MessageDigest类对字节数组进行哈希,处理相关异常,如清单12所示。
将哈希后的字节数组编码成十六进制字符。JDK中还是没有干这事的,你依旧需要创建另外一个辅助方法,有可能在你的实现中会使用位操作和位移动。
清单12. JDK的消息摘要
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.digest(bytes);
byte[] hashed = md.digest();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
对于这样简单普遍的需求,这个工作量实在太大了。现在看看Shiro是如何做同样事情的:
String hex = new Md5Hash(myFile).toHex();
当使用Shiro简化所有这些工作时,一切都非常简单明了。完成SHA-512哈希和密码的Base64编码也一样简单。
String encodedPassword = new Sha512Hash(password, salt, count).toBase64();
你可以看到Shiro对哈希和编码简化了不少,挽救了你处理在这类问题上所消耗的脑细胞。
密码
加密是使用密钥对数据进行可逆转换的加密算法。我们使用其保证数据的安全,尤其是传输或存储数据时,以及在数据容易被窥探的时候。
如果你曾经用过JDK的Cryptography API,特别是javax.crypto.Cipher类,你会知道它是一头需要驯服的极其复杂的野兽。对于初学者,每个可能的加密配置总是由一个 javax.crypto.Cipher实例表示。必须进行公钥/私钥加密?你得用Cipher。需要为流操作使用块加密器(Block Cipher)?你得用Cipher。需要创建一个AES 256位Cipher来保护数据?你得用Cipher。你懂的。
那么如何创建你需要的Cipher实例?您得创建一个非直观、标记分隔的加密选项字符串,它被称为“转换字符串(transformation string)”,把该字符串传给Cipher.getInstance静态工厂方法。这种字符串方式的cipher选项,并没有类型安全以确保你正在用 有效的选项。这也暗示没有JavaDoc帮你了解相关选项。并且,如果字符串格式组织不正确,你还需要进一步处理Checked Exception,即便你知道配置是正确的。如你所见,使用JDK Cipher是一项相当繁重的任务。很久以前,这些技术曾经是Java API的标准,但是世事变迁,我们需要一种更简单的方法。
Shiro通过引入它的CipherService API试图简化加密密码的整个概念。CipherService是多数开发者在保护数据时梦寐以求的东西:简单、无状态、线程安全的API,能够在一次方 法调用中对整个数据进行加密或解密。你所需要做的只是提供你的密钥,就可根据需要加密或解密。如下列清单13中,使用256位AES加密:
清单13. Apache Shiro的加密API
AesCipherService cipherService = new AesCipherService();
cipherService.setKeySize(256);
//创建一个测试密钥:
byte[] testKey = cipherService.generateNewKey();
//加密文件的字节:
byte[] encrypted = cipherService.encrypt(fileBytes, testKey);
较之JDK的Cipher API,Shiro的示例要简单的多:
你可以直接实例化一个CipherService - 没有奇怪或让人混乱的工厂方法;
Cipher配置选项可以表示成JavaBean - 兼容的getter和setter方法 - 没有了奇怪和难以理解的“转换字符串”;
加密和解密在单个方法调用中完成;
没有强加的Checked Exception。如果愿意,可以捕获Shiro的CryptoException。
Shiro的CipherService API还有其他好处,如同时支持基于字节数组的加密/解密(称为“块”操作)和基于流的加密/解密(如加密音频或视频)。
不必再忍受Java Cryptography带来的痛苦。Shiro的Cryptography支持就是为了减少你在确保数据安全上付出的努力。