本文翻译自Application Security With Apache Shiro,仅供参考。
当你试图保护您的应用程序时,你是否感到沮丧?你是否觉得现有的Java安全解决方案很难使用,只会让你更加困惑?本文介绍Apache Shiro,是一个Java安全框架,提供了一种简单但功能强大的方法,来保证应用的安全。本文解释了Apache Shiro的项目目标、架构哲学以及如何使用Shiro来保护你自己的应用程序。
什么是Apache Shiro?
Apache Shiro(发音为“shee-roh”,来自日语中的“城堡”)是一个功能强大且易于使用的Java安全框架,它执行身份验证、授权、加密和会话管理,可用于保护任何形式的应用 —— 从命令行应用、手机应用到最大的web和企业级应用。 Shiro为保护应用的安全,提供了API来执行以下操作(称它们为应用程序安全的4个基石更为形象):
-
身份验证 —— 验证用户身份,通常称为用户“登录”。
-
授权 —— 访问控制
-
加密 —— 保护数据
-
会话管理 —— 每个用户的时间敏感状态
Shiro还支持一些辅助功能,比如web应用程序安全性、单元测试和多线程,但这些功能的存在是为了强化上述4点。
Apache Shiro为何会诞生?
对于一个框架来说,要证明其存在价值,并让人使用它,那么就应该提供其他框架无法满足的需求。为了理解这一点,我们需要看看Shiro的历史以及它创建时其他的安全方案。
在2008年加入Apache软件基金会之前,Shiro已经5岁了,之前被称为JSecurity项目,该项目始于2003年初。在2003年,对于Java应用程序开发人员来说,并没有很多通用的安全替代方案 —— 大家都被Java身份验证和授权服务(也称为JAAS)所困扰。JAAS有很多缺点 —— 虽然它的身份验证功能尚且可以接受,但授权方面反应迟钝,使用起来令人抓狂。此外,JAAS与虚拟机级别的安全问题相关过密,例如,要决定是否在JVM中加载一个类。作为一名应用程序开发人员,我更关心应用程序最终用户可以做什么,而不是我的代码在JVM中可以做什么。
由于当时正在处理的应用程序,需要访问一个干净的、与容器无关的会话机制。当时唯一的选择是HttpSessions,这需要web容器,或EJB2.1有状态session bean,它也需要EJB容器。但是当时需要的是可以与容器解耦的东西,可以在任何环境中使用。
最后,还有加密的问题。曾有多次,当需要保证数据的安全,却发现Java的加密体系结构很难理解,除非您是加密专家。这个API到处是受査异常,用起来很麻烦。我希望有一种更干净的开箱即用的解决方案,可以根据需要轻松地加密和解密数据。
因此,回顾早在2003年之前的关于安全领域,很快就会意识到,没有任何东西可以在一个单一的、内聚的框架中满足所有这些需求。因此,JSecurity也就是后来的Apache Shiro诞生了。
为何要使用Apache Shiro?
自2003年以来,安全框架格局已经发生了很大的变化,但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软件基金会(Apache Software Foundation)的一部分,该组织是一个以社区的为基础的组织。项目开发组和用户群体随时可以提供帮助。如果商业需要,Katasoft等公司也会提供专业的支持和服务。
谁在使用Shiro?
Shiro及其前身JSecurity已在各种规模和行业公司的项目中使用多年。作为Apache软件基金会顶级项目,站点流量和使用率持续显著增长。许多开源社区也在使用Shiro,例如Spring、Grails、Wicket、Tapestry、Tynamo、Mule和Vaadin,还有更多,这里无法一一列举。
Katasoft、Sonatype、MuleSoft(主要的社交网络之一)等商业公司,以及许多纽约的商业银行都使用Shiro来保护其商用软件及网站的安全。
核心概念:Subject,SecurityManager和Realms
至此,已经介绍了Shiro的优点,现在直接开始介绍API,对它有一个初始轮廓。Shiro的架构有三个主要概念(这里没有直译了,直接使用其API接口的名字,接口即概念) —— Subject、SecurityManager和Realms。
Subject
在保护应用程序时,最相关的问题可能是:“当前用户是谁?”或者“当前用户是否允许做X?”。在编码或设计用户界面时,经常会问自己这些问题:应用程序通常是基于用户来构建的,并根据不同的用户来体现(和保护)不同的功能。因此,在考虑应用程序安全性,自然是基于用户来设计。Shiro的API中Subject就基本上代表了这种思维方式。
Subject是一个安全术语,意思是“正在交互的用户”。为什么不用“User”这个词语,因为“User”这个词通常与人有关。而在安全领域,术语“Subject”可以指一个人,也可以指第三方进程、后台帐户或其他类似的东西。它仅表示“当前与软件交互的东西”。然而,就大多数情况下,你可以把它当作是“用户”。在代码中的任何地方,可以很容易地获取Shiro中的Subject对象,如下面Listing 1所示。
// Listing 1. Acquiring the Subject
import org.apache.shiro.subject.Subject;
import org.apache.shiro.SecurityUtils;
...
Subject currentUser = SecurityUtils.getSubject();
一旦获取了Subject,就可以立即使用90%的功能,这些都是在Shiro中实现的,例如登录、注销、访问他们的会话、执行授权检查等等 —— 稍后会详细介绍。这里的关键是,对于开发人员Shiro的API是非常直观的,因为它反映了自然思维方式,基于“每个用户”进行安全控制。而Subject可以在代码中的随时获取,这就意味着:只要需要,就能进行操作。
SecurityManager
Subject的背后是SecurityManager。用Subject管理当前用户的安全操作,与此同时SecurityManager管理所有用户的安全操作。它是整个Shiro的架构的核心,类似于“保护伞”,内部引用了许多嵌套的安全组件,这些组件形成了一个架构,这里称之为对象图。然而,一旦SecurityManager和它的内部对象图配置完成,它通常就被搁置一旁,应用程序开发人员主要只和Subject打交道。
那么如何设置SecurityManager呢?这取决于应用程序。例如,一个web应用程序通常会在web.xml中配置一个Shiro Servlet Filter,由它来设置SecurityManager实例。如果你运行的是单独的应用程序,就要用另一种方式配置它。但有很多配置项。
大多数情况下,每个应用有且只有一个SecurityManager实例。它本质上是一个单例应用(尽管它不必是静态单例)。就像Shiro中几乎所有对象,包括默认的SecurityManager的实现,都是POJO,并且可以通过任何与POJO兼容的配置机制进行配置 —— 普通Java代码、Spring的XML、YAML、.properties和.ini文件等。基本上,任何能够实例化类和调用与javabeans兼容的方法的,都可以用来作为配置方法。
为此,Shiro通过基于文本的INI文件提供了一个默认的“通用方法”作为配置方案。INI易于阅读,使用简单,依赖极少。通过对象图导览的简单理解,可以看到INI能有效地配置简单的对象图,就像SecurityManager。请注意,Shiro还支持Spring的XML配置和其他方法,但我们将在这里介绍INI。
下面Listing 2中的示例显示了基于INI配置Shiro的最简单示例。
# Listing 2. Configuring Shiro with INI
[main]
cm = org.apache.shiro.authc.credential.HashedCredentialsMatcher
cm.hashAlgorithm = SHA-512
cm.hashIterations = 1024
# Base64 encoding (less text):
cm.storedCredentialsHexEncoded = false
iniRealm.credentialsMatcher = $cm
[users]
jdoe = TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJpcyByZWFzb2
asmith = IHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbXNoZWQsIG5vdCB
在Listing 2中,我们看到了用于配置SecurityManager实例的INI文件配置的示例。在这里,INI分两个部分:[main]和[users]。
[main]部分是配置SecurityManager对象以及或者SecurityManager使用的任何对象(如Realms)。在这个例子中,我们看到配置了两个对象:
- cm对象,是Shiro的HashedCredentialsMatcher类的一个实例。如上所示,cm实例的各种属性都是通过“.”语法配置的 —— 这种约定的方法在Listing 3中也用来配置IniSecurityManagerFactory,用于描述对象图的导览和属性设置。
- 在INI格式中定义的iniRealm对象,它是SecurityManager用来表示用户帐户的组件。
[users]部分可以指定一个用户帐户静态列表 —— 对于简单的应用或测试时很方便。
这里的介绍目的,并不是技术细节,而是了解INI是配置Shiro的一种简单方法。有关INI配置的更多详细信息,请参阅Shiro的文档。
// Listing 3. Loading shiro.ini Configuration File
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.util.Factory;
...
//1. Load the INI configuration
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//2. Create the SecurityManager
SecurityManager securityManager = factory.getInstance();
//3. Make it accessible
SecurityUtils.setSecurityManager(securityManager);
在Listing 3中,我们看到这个简单示例中的三个步骤:
- 加载INI配置文件,后面用于配置SecurityManager及其组件。
- 根据配置文件,创建SecurityManager实例(使用Shiro的工厂概念,用来表示工厂方法设计模式)。
- 让应用程序可以访问SecurityManager单例。在这个简单的例子中,将其设置为VM-static单例,但这通常是不必要的 —— 应用的配置机制可以确定是否需要使用静态内存。
Realms
Shiro的第三个也是最后一个核心概念是“Realm”。Realm充当Shiro和受保护应用程序数据之间的“桥梁”或“连接器”。也就是说,当需要与安全相关的数据(如用户账户)进行实际交互,以执行身份验证(登录)和授权(访问控制)时,Shiro会从一个或多个为应用程序配置的Realms中查找这些内容。
从这个意义上说,Realm本质上是一个安全特定的DAO:它封装了数据源的连接细节,并使Shiro可以根据需要使用相关的数据。当配置Shiro时,必须指定至少一个Realm用于身份验证以及/或者授权。可以配置多个Realm,但至少需要一个。
Shiro提供了开箱即用的Realms来连接许多安全数据源(又名目录),如LDAP、关系数据库(JDBC)、文本配置源(如INI和propeties文件)等。如果默认Realms不能满足需求,可以插入自己的Realms实现来表示自定义数据源。下面的Listing 4是配置Shiro(通过INI文件),以使用LDAP目录作为应用程序Realms之一的示例。
# Listing 4. Example realm configuration snippet to connect to LDAP user data store
[main]
ldapRealm = org.apache.shiro.realm.ldap.JndiLdapRealm
ldapRealm.userDnTemplate = uid={0},ou=users,dc=mycompany,dc=com
ldapRealm.contextFactory.url = ldap://ldapHost:389
ldapRealm.contextFactory.authenticationMechanism = DIGEST-MD5
至此,我们已经了解如何设置一个基本的Shiro环境,让我们来讨论一下作为开发人员,如何使用这个框架。
身份验证
身份验证是验证用户身份的过程。也就是说,当用户使用应用程序进行身份验证时,他们实际上是在证明自己是合法用户。这个过程也被称为login。这通常有三个步骤。
- 收集用户的标识信息(称为principals),和支持身份的证明(称为credentials)。
- 向系统提交principals和credentials。
- 如果提交的principals和credentials与系统里的数据相匹配,则认定该用户已通过身份验证。如果不匹配,则认为用户未通过身份验证。
大家都熟悉的一个常见示例就是用户名/密码登录。当大多数用户登录应用时,他们通常需要提供用户名(principals)和密码(credentials)。如果存储在系统中的密码(或密码的表示方式)与用户提供的匹配,则认为已通过身份验证。
同样的验证过程,Shiro的方式简单而又直观。正如其所言,Shiro有一个以Subject为中心的API —— 程序在运行时,任何要用Shiro做的事情都是通过与当前Subject交互来达成的。因此,要登录只需调用Subject的login方法,并传给方法一个AuthenticationToken实例作为参数,该实例包含了需要提交的principals和credentials(在本例中分别指用户名和密码)。这个例子如下面的Listing 5所示。
// Listing 5. Subject Login
//1. Acquire submitted principals and credentials:
AuthenticationToken token = new UsernamePasswordToken(username, password);
//2. Get the current Subject:
Subject currentUser = SecurityUtils.getSubject();
//3. Login:
currentUser.login(token);
如你所见,Shiro的API轻而易举的体现了整个过程。后面会继续看到Subject的操作都是这种简单的风格。当调用login方法后,SecurityManager将接收AuthenticationToken,并将其分发到一个或多个已配置的Realms,并让其执行必要身份验证。每个Realms都能根据需要对提交的AuthenticationToken作出回应。但是,如果登录失败会发生什么呢?如果用户密码不正确怎么办?这可以通过对Shiro的运行异常AuthenticationException来处理,如Listing 6所示。
// Listing 6. Handle Failed Login
//3. Login:
try {
currentUser.login(token);
} catch (IncorrectCredentialsException ice) { …
} catch (LockedAccountException lae) { …
}
…
catch (AuthenticationException ae) {…
}
通过捕获AuthenticationException其中的一个子类并作出特定的反应,或者捕获AuthenticationException做通用地处理(例如,向用户显示一个通用的“不正确的用户名或密码”消息)。根据应用程序需求做选择。
登录成功后,即表示通过了身份验证,允许使用应用了。但是仅仅通过了身份并不意味着他们可以在你的应用中为所欲为。这就引出了下一个问题:“如何控制用户可以做什么或不能做什么?”决定用户可以做什么被称为授权。接下来将介绍Shiro如何启用授权。
授权
授权本质上就是访问控制 —— 控制用户在应用中可以访问什么,例如资源、网页等等。多数时候用户通过使用角色和权限等概念来进行访问控制。也就是说,用户通常基于分配给用户的角色和/或权限,表示能做某事或不做某事。应用也是基于角色和权限来控制提供哪些功能。正如所料,Subject的API很容易执行角色和权限检查。例如,Listing 7中的代码段显示了如何检查Subject是否被分配了某个角色。
// Listing 7. Role Check
if ( subject.hasRole(“administrator”) ) {
//show the ‘Create User’ button
} else {
//grey-out the button?
}
如上所示,应用通过检查角色的访问控制的权限,来决定启用或禁用功能。
进行权限检查是执行授权的另一种方式。上面示例中这种的角色检查方法有一个重大缺陷:不能在运行时添加或删除角色。角色名在代码内被硬编码了,所以如果你改变了角色名和/或配置,你的代码就会被破坏!如果你需要在运行时改变角色的含义,或者根据需要添加或删除角色,你必须依赖其他东西。
为此,Shiro提出了它的权限概念。权限是一个原始的功能声明,例如“开门”,“建一条博客”,“删除jsmith”等。由于权限反映应用的原始功能,只需要在更改应用的功能时更改权限检查。反之,可以在运行时根据需要为角色或用户分配权限。
举个例子,如下面Listing 8所示,可以重写之前的角色检查,而使用权限检查。
// Listing 8. Permission Check
if ( subject.isPermitted(“user:create”) ) {
//show the ‘Create User’ button
} else {
//grey-out the button?
}
这样,任何被赋予“user:create”权限的角色或用户都可以点击“create user”按钮,并且这些角色和分配甚至可以在运行时更改,提供了一个非常灵活的安全模型。
“user:create”字符串是遵循某些解析约定的,表示权限字符串的一个例子。Shiro使用其开箱即用通配符(WildcardPermission),来支持这种约定。尽管超出了本文的范围,但后面将看到,在创建安全策略时,WildcardPermission可以非常灵活,甚至支持诸如实例级访问控制。
// Listing 9. Instance-Level Permission Check
if ( subject.isPermitted(“user:delete:jsmith”) ) {
//delete the ‘jsmith’ user
} else {
//don’t delete ‘jsmith’
}
这个例子表明,如果需要,在控制对单个资源的访问时,甚至可以控制到非常细粒度的实例级别。如果愿意,甚至可以发明自己的权限语法。更多信息请参见Shiro Permission。最后,就像身份验证一样,上述调用最终也会送达到SecurityManager,后者将调用一个或多个Realms来做出访问控制决策。这允许Realm根据需要来响应身份验证和授权操作。
以上就是Shiro授权能力的简要概述。虽然大多数安全框架仅限于身份验证和授权,但Shiro提供了更多的功能。接下来,将讨论Shiro的高级会话管理功能。
会话管理
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中有两个方法,如下面的例子所示。
// Listing 10. Subject’s Session
Session session = subject.getSession();
Session session = subject.getSession(boolean create);
如所见,这些方法在概念上与HttpServletRequest API相同。第一个方法将返回Subject现有的会话,如果没有,则创建一个新的会话并返回。第二个方法接受一个boolean参数,用于决定是否在会话不存在时创建新会话。一旦获得了会话,您就可以几乎与HttpSession一样使用它。Shiro团队认为HttpSession API对Java开发人员来说是最顺手的,因此保留了它的大部分特性。当然,最大的区别在于,你可以在任意应用中使用Shiro会话,而不仅仅是web应用程序。Listing 11示例显示了这种相似性。
// Listing 11. Session methods
Session session = subject.getSession();
session.getAttribute(“key”, someValue);
Date start = session.getStartTimestamp();
Date timestamp = session.getLastAccessTime();
session.setTimeout(millis);
加密
加密是隐藏或模糊数据的过程,使不怀好意者无法破解。Shiro的目标就是使JDK的加密功能简化并且易用。
重要的是,加密并不是特定于Subject,它是Shiro的API的一个功能,其功能并不特定于Subject。在其他任何地方都能使用Shiro的加密功能,即使没有使用Subject。Shiro加密功能集中于两个方面,加密散列码(即消息摘要)和加密密码。后面进一步探讨这两个方面。
散列码
如果使用过JDK的MessageDigest类,很快就意识到它有点麻烦。它有一个不那么优雅的API,是基于工厂模式的静态方法,而不是面向对象,而且被迫捕获那些可能永远不需要捕获的已检异常。如果需要十六进制编码或base64编码的消息摘要作为输出,就得靠自己 —— JDK对这两种格式都没有标准支持。Shiro通过哈希API解决了这些问题,简洁且直观。
举个相对常见的例子,用md5对文件进行散列并确定该散列的十六进制值。这称为“校验和”,在提供文件下载时经常使用 —— 用户可以对下载的文件执行自己的MD5哈希,并断言他们的校验和与网站上下载的是否匹配。如果匹配,用户就可以充分假设文件在传输过程中没有被篡改。
如果不使用Shiro,你可能会这样做:
- 将文件转换为字节数组。JDK并没有提供任何现成的方法,所以需要自己创建一个方法作为辅助,在方法中需要打开FileInputStream,使用字节缓冲区(byte buffer),并抛出适当的ioexception异常,等等。
- 使用MessageDigest类对字节数组进行散列,处理适当的异常,如下面的清单12所示。
- 将散列后的字节数组编码为十六进制字符。对于这个功能JDK中也没有提供任何辅助方法,所以需要创建另一个辅助方法,实现时可能会涉及到位操作和位移位。
// Listing 12. JDK’s MessageDigest
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的加密API,特别是javax.crypto.Cipher这个类,就知道它是非常复杂,难以使用。对于初学者,每个可能的密码配置都由javax.cryptx.Cipher的一个实例表示。需要进行公钥/私钥加密吗?你用Cipher。需要对流操作使用分组密码?你用Cipher。需要创建256位的AES密码来保护数据?你用Cipher。结果可想而知。
如何创建所需的Cipher实例?创建一个既复杂、又不直观,用某种标记分隔的字符串,作为密码选项,称之为“转换字符串”,并将其传递给Cipher.getInstance这个静态工厂方法。用字符串方式作为加密选项,没有类型安全来确保使用的是否是有效的选项。这也意味着没有JavaDoc来帮助你理解相关选项。即使配置是正确的,如果字符串格式不正确,依然需要处理已检异常。可见,使用JDK中的Ciphers相当麻烦。长期以来,这些技术作为Java API的标准,但时代变了,需要一种更简单的方法。
Shiro试图通过引入CipherService API来简化加密密码的整个概念。CipherService:简单、无状态、线程安全的API,仅通过调用一个方法,就可以完全进行加密或解密的工作,这正是大多数开发人员在保护数据时所需要的。只需要提供密钥,就可以根据需要进行加密或解密。例如,使用256位AES进行加密,如下面的Listing 13所示:
// Listing 13. Apache Shiro’s Encryption API
AesCipherService cipherService = new AesCipherService();
cipherService.setKeySize(256);
//create a test key: 秘钥
byte[] testKey = cipherService.generateNewKey();
//encrypt a file’s bytes: 加密
byte[] encrypted = cipherService.encrypt(fileBytes, testKey);
相比JDK的Cipher API,Shiro更简单:
- 直接实例化一个CipherService —— 没有那些奇怪或难理解的工厂方法。
- 用javabeans的形式表示密码配置选项,使用getter和setter方法 —— 没有奇怪且难明其意的“转换字符串”。
- 调用单个方法进行加密和解密。
- 没有强制的已检异常。如果愿意,可以捕获Shiro的CryptoException。
Shiro的CipherService API还有其他好处,例如支持基于字节数组的加密/解密(称为“块”操作)以及基于流的加密/解密(例如加密音频或视频)。
Java的加密不再痛苦。Shiro对加密的支持,能简化数据安全的工作。
对Web的支持
最后,我们将简要介绍Shiro对web的支持。Shiro提供了一个健壮的web支持模块来保护web应用。在web应用中设置Shiro很简单。唯一需要做的就是在web.xml中定义一个Shiro Servlet Filter过滤器。Listing 14显示了这些代码。
<!-- Listing 14. ShiroFilter in web.xml -->
<filter>
<filter-name>ShiroFilter</filter-name>
<filter-class>
org.apache.shiro.web.servlet.IniShiroFilter
</filter-class>
<!-- no init-param means load the INI config
from classpath:shiro.ini -->
</filter>
<filter-mapping>
<filter-name>ShiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
过滤器可以读取上述shiro.ini配置文件,因此无论部署环境如何,都有一致的配置体验。配置好之后,Shiro Filter将过滤每个请求,并确保在请求期间可以访问特定于请求的Subject。而且由于过滤每个请求,因此还能执行某些特定安全的逻辑,以确保只有满足这些逻辑的请求才能通过。
基于URL的过滤器链
Shiro通过其创新的URL过滤器链功能支持特定于安全的过滤规则。它能通过匹配模式来为URL指定特定的过滤器链。这意味着,Shiro的过滤机制在执行安全规则(或规则组合)方面有很大的灵活性 —— 比单独在web.xml中定义过滤器要灵活得多。Listing 15显示了Shiro INI中的配置片段。
# Listing 15. Path-specific Filter Chains
[urls]
/assets/** = anon
/user/signup = anon
/user/** = user
/rpc/rest/** = perms[rpc:invoke], authc
/** = authc
如上所示,在web应用的INI配置文件中有一节[urls],专门用于配置URL。每一行中,等号左侧的值表示web应用的相对路径。右边的值定义了一个过滤器链 —— 这是一个有序的、以逗号分隔的Servlet过滤器列表,并对给定路径进行执行(左边的值)。每个过滤器都是一个普通的Servlet过滤器,但是在上面例子中看到的这些过滤器名称(anon、user、perms、authc),都是Shiro提供的开箱即用的与安全相关的特殊过滤器。可以混合搭配这些过滤器,以创建自定义的安全体验。还可以指定任何其他现有的Servlet过滤器。
这比使用web.xml要好得多,在web.xml中,定义一个过滤器,然后定义过滤模式,这两者之间是分开的,并无联系。而在Shiro的配置文件方法中,路径以及需要执行的过滤器链一一对应,一目了然。如愿意,可以只在web.xml中定义Shiro Filter,并在Shiro .ini中定义所有其他过滤器和过滤器链,这比web.xml更简洁、更容易理解的过滤器链定义机制。即便没有使用过Shiro,不了解其安全特性,这个小小的便利性也让Shiro值得一试。
JSP标签库
Shiro还提供了一个JSP标签库,可以根据Subject的状态,控制JSP页面的输出。一个常用的例子是在用户登录后显示Hello (username)文本。但如果还未登录,是匿名的,可能要显示其的内容是“Hello!立即注册”。Listing 16显示了如何使用Shiro的JSP标签库来支持这一功能。
<!-- Listing 16. JSP Taglib Example -->
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
...
<p>Hello
<shiro:user>
<!-- shiro:principal prints out the Subject’s main
principal - in this case, a username: -->
<shiro:principal/>!
</shiro:user>
<shiro:guest>
<!-- not logged in - considered a guest. Show
the register link: -->
! <a href=”register.jsp”>Register today!</a>
</shiro:guest>
</p>
还有其他一些标记供使用,根据角色、权限,以及是否经过身份验证、是否被“Remember Me”服务记住,还是一个匿名客户来控制输出。
Shiro还支持许多其他特定于web的功能,如“Remember Me”服务、REST和基本身份验证,当然,如果想使用Shiro的原生企业会话,还支持透明的HttpSession。请参阅Apache Shiro web文档了解更多信息。
web会话管理
最后,值得一提的是Shiro对web环境下会话的支持。
默认的Http会话
对于web应用,Shiro默认使用常用的Servlet容器会话。也就是说,当调用subject.getSession()和subject.getSession(boolean)方法时,Shiro将返回由Servlet容器的HttpSession支持的会话实例。这种方法的美妙之处在于,调用subject.getSession()的业务层代码,与Shiro会话实例交互 —— 它不“知道”它正在使用基于web的HttpSession对象。对于保持架构层之间的清晰分离,这是非常好的。
Shiro在Web层的原生会话
如果你在web应用中启用了Shiro的原生会话管理,因为需要Shiro的企业会话功能(比如容器无关的集群),那么当然希望HttpServletRequest.getSession()和HttpSession API与原生会话合作,而不是servlet容器会话。重构任何使用HttpServletRequest和HttpSession API的代码肯定比直接使用Shiro的Session API麻烦得多。既然使用Shiro,那当然不必那么做。Shiro完全实现了Servlet规范中的会话,在web应用中完全可以使用Shiro原生会话。这意味着无论何时你调用相应的HttpServletRequest或HttpSession方法,Shiro都会将这些调用委托给它内部的原生会话API。最终的结果是,即使使用Shiro的“原生”企业会话管理,也不必更改你的web代码 —— 这确实是一个非常方便(也是必要的)的功能。
附加特性
Apache Shiro框架中还有其他一些对保护Java应用程序安全有用的功能,例如:
- 支持线程和并发,在线程之间维护Subject(支持Executor和ExecutorService)
- 支持Callable和Runnable作为特定Subject来执行逻辑
- 支持以另一个Subject的“运行”程序(例如,在管理应用中特别有用)
- 对测试套件支持,使得对Shiro安全代码进行单元测试和集成测试变得很容易。
框架的限制
Apache Shiro并非十全十美,尽管希望如此 —— 但它不能毫不费力地解决所有安全问题。Shiro没有解决的一些事情还是值得了解一下:
- 虚拟机级别的安全问题:Apache Shiro目前不处理虚拟机级别的安全,例如基于访问控制策略,防止类加载器中加载某些类。然而,Shiro可以与现有的JVM安全操作集成,并不是不可能 —— 只是在这个项目没有人做。
- 多阶段身份验证:Shiro目前原生不支持“多阶段”身份验证,即用户可能通过一种机制登录,然后被要求使用不同的机制再次登录。在基于Shiro的应用中,这是通过应用程序提前收集所有需要的信息,然后与Shiro交互来完成的。这个功能很有可能在未来的Shiro版本中得到支持。
- Realm写操作:目前所有Realm的实现都支持“读”操作来获取身份验证和授权数据,用以执行登录和访问控制。不支持“写”操作,如创建用户帐户、组和角色,或关联户与角色、组和权限。这是因为支持这些操作的数据模型在不同的应用中差异很大,并且很难强制所有Shiro用户去执行一个统一的write API。
即将推出
Apache Shiro的社区日渐壮大,Shiro的功能也日臻完善。在即将发布的版本中,你可能会看到:
- 更整洁的Web过滤器机制,允许更多的插件过滤支持,而无需子类化。
- 支持组合而非继承的可插拔默认Realm实现。能够插入查找身份验证和授权数据的组件,而不是要求实现一个Shiro Realm的子类。
- 强大的OpenID和OAuth(也可能是混合型)客户端支持
- 支持验证码
- 对于100%无状态的应用程序(例如,许多REST环境)更容易配置。
- 通过请求/响应协议进行多阶段身份验证。
- 通过AuthorizationRequest进行粗粒度授权。
- 用于安全断言查询的ANTLR语法(例如( role(admin) && (guest || !group(developer)) )
综述
Apache Shiro是一个功能齐全、健壮且通用的Java安全框架,完全使用它来保护应用。将应用安全的概念简化为:认证、授权、会话管理和加密,让其在实际应用中更容易理解和实现。Shiro简单的架构和JavaBeans兼容性使得它几乎可以在任何环境中进行配置和使用。额外的web支持和辅助功能(如多线程和测试支持)完善了框架,为应用安全提供了“一站式”服务。Apache Shiro的开发团队将继续前进,完善代码库并对社区提供支持。随着开源和商业应用的持续发展,Shiro有望变得更加强大。
资源
- Apache Shiro主页。
- Shiro的下载页面,为Maven和Ant+Ivy用户提供更多信息。
- Apache Shiro的文档页面,包括指南和参考手册
- Apache Shiro的演示视频和PPT,由项目的PMC主席Les Hazlewood提供。
- 其他Apache Shiro文章和演示
- Apache Shiro邮件列表和论坛。
- Katasoft —— 这家公司提供Apache Shiro专业支持和应用安全产品。
作者简介
Les Hazlewood是Apache Shiro PMC(Project Management Committee,项目管理委员会)主席、Katasoft的联合创始人和首席技术官,Katasoft是一家专注于应用安全产品和Apache Shiro专业支持的初创企业。Les有10年的专业Java开发人员和企业架构师经验,曾在Bloomberg、Delta Airlines和JBoss担任高级职位。Les积极参与开源开发已经超过9年了,致力于或贡献了诸如Spring Framework、Hibernate、JBoss、OpenSpaces,当然还有Apache Shiro的前身JSecurity等项目。Les目前住在加利福尼亚州的圣马特奥,在不编程的时候练习剑道并学习日语。