引言
以此篇博客为引,开启一个新的专栏分类——Shiro。
之前在工作中有比较快速的学习过Shiro安全框架,但经过一年的荒废,已经不是很熟悉了,通过这个系列,深入研究和学习Shiro的一些知识,填补安全管理方面的知识漏洞。使我们在web 开发领域更具竞争力,不做只会CRUD的程序员!
一、Shiro介绍
Shiro是一个Java安全框架,执行身份验证、授权、密码、会话管理。Shiro是Apache 的一个开源项目,前身是JSecurity 项目,始于2003年初。
Shiro 可以为任何应用提供安全保障 - 从命令行应用、移动应用到大型网络及企业应用。
shiro 解决了应用安全的四要素:
- 认证 - 用户身份识别,常被称为用户“登录”;
- 授权 - 访问控制;
- 密码加密 - 保护或隐藏数据防止被偷窥;
- 会话管理 - 每用户相关的时间敏感的状态。
同时,Shiro另外支持了一些辅助特性:如 Web 应用安全、单元测试和多线程,它们的存在强化了上面提到的四个要素。
二、Shiro的优势
从 2003 年至今,框架选择方面的情况已经改变了不少,但今天仍有令人信服的理由让你选择 Shiro。其实理由相当多,Apache Shiro:
1、易于使用 - 易用性是这个项目的最终目标。应用安全有可能会非常让人糊涂,令人沮丧,并被认为是“必要之恶”【译注:比喻应用安全方面的编程。】。若是能让它简化到新手都能很快上手,那它将不再是一种痛苦了。
2、广泛性 - 没有其他安全框架可以达到 Apache Shiro 宣称的广度,它可以为你的安全需求提供“一站式”服务。
3、灵活性 - Apache Shiro 可以工作在任何应用环境中。虽然它工作在 Web、EJB 和 IoC 环境中,但它并不依赖这些环境。Shiro 既不强加任何规范,也无需过多依赖。
4、Web 能力 - Apache Shiro 对 Web 应用的支持很神奇,允许你基于应用 URL 和 Web 协议(如 REST)创建灵活的安全策略,同时还提供了一套控制页面输出的 JSP 标签库。
5、可插拔 - Shiro 干净的 API 和设计模式使它可以方便地与许多的其他框架和应用进行集成。你将看到 Shiro 可以与诸如 Spring、Grails、Wicket、Tapestry、Mule、Apache Camel、Vaadin 这类第三方框架无缝集成。
6、支持 - Apache Shiro 是 Apache 软件基金会成员,这是一个公认为了社区利益最大化而行动的组织。项目开发和用户组都有随时愿意提供帮助的友善成员。
三、核心概念
Shiro的核心概念有三个:Subject,SecurityManager 和 Realms。
3.1 Subject
subject 被Shiro 描述为一个主体,对于web应用来说,可以简单理解为用户。
这里我们来阐述一个Shiro设计的重要理念,即以主体为展开的安全体系构建。引用一段话:
在考虑应用安全时,你最常问的问题可能是“当前用户是谁?”或“当前用户允许做 X 吗?”。当我们写代码或设计用户界面时,问自己这些问题很平常:应用通常都是基于用户故事构建的,并且你希望功能描述(和安全)是基于每个用户的。所以,对于我们而言,考虑应用安全的最自然方式就是基于当前用户。Shiro 的 API 用它的 Subject 概念从根本上体现了这种思考方式。
在应用程序中,我们可以在任何地方获取当前操作的用户主体:
import org.apache.shiro.subject.Subject;
import org.apache.shiro.SecurityUtils;
...
Subject currentUser = SecurityUtils.getSubject();
获得Subject 后,通过这个对象,我们可以对其进行绝大多数安全操作:登录、登出、访问会话、执行授权检查等。
Shiro 的api非常直观,它反映了开发者以“每个用户” 思考安全控制的自然趋势。
3.2 SecurityManager
Subject 的幕后推手是 SecurityManager,Subject 代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。
SecurityManager 是 Shiro 框架的核心,充当“保护伞”,引用了多个内部嵌套安全组件,它们形成了对象图。但是,一旦 SecurityManager 及其内部对象图配置好,它就会退居幕后,应用开发人员几乎把他们的所有时间都花在 Subject API 调用上。
一个应用只需要一个 SecurityManager,是一个单例对象。它的缺省实现是POJO,Shiro 里的其他组件也是一样。因此,可以用POJO兼容的任何配置机制进行配置:普通的Java代码、Spring xml、YAML、和 ini 文件等。基本上,能够实例化类和调用JavaBean兼容方法的任何配置形式都可以。
比如,通过ini文件进行配置:
[main]
cm = org.apache.shiro.authc.credential.HashedCredentialsMatcher
cm.hashAlgorithm = SHA-512
cm.hashIterations = 1024
# Base64 encoding (less text):
cm.storedCredentialsHexEncoded = falseiniRealm.credentialsMatcher = $cm
[main] 段落是配置SecurityManager 对象及其使用的其他任何对象(如 Realm) 的地方,在上面的示例中,我们看到了两个对象:
1、cm对象,是Shiro 的HashedCredentialsMatcher 类实例,cm 的各属性通过“嵌套点”语法进行配置。
2、iniRealm对象,被 SecurityManager 用来表示以INI 格式定义的用户账户。
然后,我们在Java代码中,可以轻而易举的获得 SecurityManager对象了:
//1. 装入 INI 配置
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");//2. 创建 SecurityManager
SecurityManager securityManager = factory.getInstance();//3. 使其可访问
SecurityUtils.setSecurityManager(securityManager);
3.3 Realm
Realm 充当了 Shiro 与应用安全数据间的“桥梁”或者“连接器”。当切实与像用户帐户这类安全相关数据进行交互,执行认证(登录)和授权(访问控制)时,Shiro 会从应用配置的 Realm 中查找很多内容。
从某种意义上讲,Realm 实际上就是一个安全相关的 DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给 Shiro。
注意:在配置Shiro 时,必须指定至少一个 Realm ,可以配置多个。
Shiro 内置了一些Realm ,支持多种数据源的连接,如JDBC、LDAP、INI文件的连接等。另外,可以自定义Realm 实现,方便个性化的应用场景。
四、典型的安全场景概述
应用安全的四要素:认证、授权、会话管理、加密。
4.1 认证
虽然有些武断,但是一般web 应用认证就是登录功能。也就是说,当用户使用应用进行认证时,他们就在证明他们就是自己所说的那个人。
这是一个典型的三步过程:
1、收集用户身份信息,成为当事人(principal),以及身份的支持证明,称为证书(Credential)。
2、将当事人和证书提交给系统。
3、如果提交的证书与系统期望的该用户身份(当事人)匹配,该用户就被认为是经过认证的,反之则被认为未经认证的。
Shiro 以简单直观的方式支持同样的流程。Shiro 有一套以Subject 为中心的API,几乎你想要用 Shiro 在运行时完成的所有事情都能通过与当前执行的 Subject 进行交互而达成。因此,要登录 Subject,只需要简单地调用它的 login 方法。传入表示被提交当事人和证书(在这种情况下,就是用户名和密码)的 AuthenticationToken 实例。
//1. 接受提交的当事人和证书:
AuthenticationToken token =
new UsernamePasswordToken(username, password);//2. 获取当前 Subject:
Subject currentUser = SecurityUtils.getSubject();
//3. 登录:
currentUser.login(token);
可以看到,Shiro的操作及其简洁和自然,这也是Shiro 惯有的风格。在调用 login()方法后,SecurityManager 会收到AuthenticationToken,并将其发送给已配置的 Realm,执行必须的认证检查,以往我们手动去数据库中进行校验和匹配的时代已经过去了,这些所有的操作,全部由Shiro 帮我们自动完成。当数据经过Realm 的检查后发现无法匹配,那么Shiro 就会返回 AuthenticationException 异常的子类,通过这些子类,我们可以精确的控制想要返回给用户的错误信息:
try {currentUser.login(token);
} catch (IncorrectCredentialsException ice) {…
} catch (LockedAccountException lae) {…
}
…
catch (AuthenticationException ae) {…
}
如果没有抛出任何异常,则证明 Subject 登录成功,就被认为是已认证的。
4.2 授权
授权实质上就是访问控制,控制已认证的用户能够访问应用的哪些内容,如资源、页面等。
多数用户执行访问控制是通过 角色 + 权限 的概念来完成的。角色是所有用户个体的一个分组,如管理员、普通用户、商家等;而权限 则表示具体能够操作的行为,比如查询所有用户、删除某些用户、修改信息等等,是与具体应用资源直接挂钩的。
用户、角色和 权限三者往往通过 角色 来进行转换,用户和权限之间通常不进行直接绑定:
我们可以通过shiro的校验方法,来便捷地实现分支语句:
if ( subject.hasRole("administrator") ) {// 显示‘Create User’按钮
} else {// 按钮置灰?
}
虽然,在概念上,权限与角色直接挂钩,但其最终效果还是要落实到具体的某个用户是否具有某个权限,为此,Shiro也为我们提供了相应的校验方法:
if ( subject.isPermitted("user:create") ) {// 显示‘Create User’按钮
} else {// 按钮置灰?
}
这样,任何具有“user:create”权限的角色或用户都可以点击‘Create User’按钮,并且这些角色和指派甚至可以在运行时改变,这给你提供了一个非常灵活的安全模型。
上例中,"user:create" 字符串是一种遵循特定规则的权限描述符,这在后面的文章中会单独介绍。具体详情可了解:http://shiro.apache.org/permissions.html
上面这些权限的调用,最终都会发送到SecurityManager中,它会咨询 Realm 做出自己的访问控制决定。必要时,还允许单个 Realm 同时响应认证和授权操作。
4.3 会话管理
在以往的Servlet应用中,我们最常使用的会话对象就是 HttpSession 对象。
在Shiro 中,也有属于自己的会话管理机制和用户的会话对象。Shiro 允许开发者在任何应用或架构层一致地使用 Session API。
它为任何应用(从小型后台独立应用到大型集群 Web 应用)提供了一个会话编程范式。这意味着,那些希望使用会话的应用开发者,不必被迫使用 Servlet 或 EJB 容器了。或者,如果正在使用这些容器,开发者现在也可以选择使用在任何层统一一致的会话 API,取代 Servlet 或 EJB 机制。
Shiro 会话最重要的一个好处或许就是它们是独立于容器的。这个特性的作用非常巨大,设想一下会话集群。对集群会话来讲,支持容错和故障转移有多少种容器特定的方式?Tomcat 的方式与 Jetty 的不同,而 Jetty 又和 Websphere 不一样,等等。但通过 Shiro 会话,你可以获得一个容器无关的集群解决方案。
Shiro 的架构允许可插拔的会话数据存储,如企业缓存、关系数据库、NoSQL 系统等。这意味着,只要配置会话集群一次,它就会以相同的方式工作,跟部署环境无关 - Tomcat、Jetty、JEE 服务器或者独立应用。不管如何部署应用,毋须重新配置应用。
获取当前用户的Session 对象,我们可以使用下面这样的方法:
Session session = subject.getSession();
Session session = subject.getSession(boolean create);
上面这些方法在概念上等同于HttpServletRequest API。第一个方法会返回 Subject 的现有会话,或者如果还没有会话,它会创建一个新的并将之返回。第二个方法接受一个布尔参数,这个参数用于判定会话不存在时是否创建新会话。一旦获得 Shiro 的会话,你几乎可以像使用 HttpSession 一样使用它。Shiro 保留的 HttpSession 的使用体验,但不同的是 Shiro 可以在任何应用中使用会话机制,不仅限于Web应用。
Shiro Session 的一些方法:
Session session = subject.getSession();
session.getAttribute("key", someValue);
Date start = session.getStartTimestamp();
Date timestamp = session.getLastAccessTime();
session.setTimeout(millis); ...
4.4 加密
在加密方面,Shiro 尽可能的简化加密处理的步骤,并让JDK的加密支持可用。
注意一点,加密并不是特定于Subject 的,加密的特性是 Shiro 的一部分,但不特定于仅仅对 Subject 的处理。我们可以在任何地方使用 Shiro 的加密支持,甚至在不使用 Subject 的情况下。
对于加密支持,Shiro 真正关心的两个领域是加密哈希(又名消息摘要)和加密密码。
传统的JDK加密操作过于复杂,而且是基于笨拙的工厂静态方法api,它并不是面向对象的设计:
try {MessageDigest md = MessageDigest.getInstance("MD5");md.digest(bytes);byte[] hashed = md.digest();
} catch (NoSuchAlgorithmException e) {e.printStackTrace();
}
如果在某些场景下频繁会用到哈希加密的情况,那么开发者就必须要封装之后才能比较轻松的使用JDK 的加密支持,否则将会是一个不小的工作量。Shiro 为我们提供了这样的便捷,对于类似的功能,只需要简单的几行代码就可以完成,同时也省去了一些老旧的和不必要的异常捕获:
// 面向对象的MD5加密方式
String hex = new Md5Hash(myFile).toHex();
// SHA-512
String encodedPassword = new Sha512Hash(password, salt, count).toBase64();
刚刚提到的这些是一些必要的哈希算法API,另一个Shiro 关心的事情就是 加密密码。
我们知道,加密是使用密钥对数据进行可逆转换的加密算法。我们使用其保证数据的安全,尤其是传输或存储数据时,以及在数据容易被窥探的时候。
同样 JDK 的加密API也非常复杂和难用,而Shiro 通过引入它的 CipherService API 试图简化加密密码的整个概念。
CipherService 是多数开发者在保护数据时梦寐以求的东西:简单、无状态、线程安全的 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 还有其他好处,如同时支持基于字节数组的加密 / 解密(称为“块”操作)和基于流的加密 / 解密(如加密音频或视频)。
五、Web支持
Shiro 附带了一个帮助保护web应用的强健的web 支持模块,就目前大火的spring boot框架,除了配置基本的依赖和Shiro 的核心单例组件 - SecurityManager 之外,可能剩下的就只是要配置一个Shiro Servlet 过滤器了。
关于Shiro 的过滤器,后面的文章还会详细介绍,总之,当我们完成了 这些前期的必要的配置工作后,Shiro Filter 就会过滤每个请求,并确保在请求期间特定的 Subject 是可访问的。同时由于它过滤了每个请求,你可以执行安全特定的逻辑以保证只有满足一定标准的请求才被允许通过。
5.1 URL 过滤器
Shiro 通过其创新的 URL 过滤器链功能支持安全特定的过滤规则。
[urls]
/assets/** = anon
/user/signup = anon
/user/** = user
/rpc/rest/** = perms[rpc:invoke], authc
/** = authc
请不要在意这些配置形式,感受它所代表的含义和强大的特性内核。
对于每一行,等号左边的值表示相对上下文的 Web 应用路径。等号右边的值定义了过滤器链 - 一个逗号分隔的有序 Servlet 过滤器列表,它会针对给出的路径进行执行。每个过滤器都是普通的 Servlet 过滤器,你看到的上面的过滤器名字(anon,user,perms,authc)是 Shiro 内置的安全相关的特殊过滤器。你可以搭配这些安全过滤器来创建高度定制的安全体验。你还可以指定任何其他现有的 Servlet 过滤器。
5.2 Web 会话管理
对于 Web 应用,Shiro 缺省将使用我们习以为常的 Servlet 容器会话作为其会话基础设施。即,当你调用 subject.getSession() 和 subject.getSession(boolean) 方法时,Shiro 会返回 Servlet 容器的 HttpSession 实例支持的 Session 实例。
这种方式的曼妙之处在于调用 subject.getSession() 的业务层代码会跟一个 Shiro Session 实例交互 - 还没有“认识”到它正跟一个基于 Web 的 HttpSession 打交道。这在维护架构层之间的清晰隔离时,是一件非常好的事情。
另外,当需要使用与容器无关的会话特性时,我们也可以开启Shiro 的原生会话管理。不同于传统的HttpServletRequest.getSession()和 HttpSession API只能和Servlet 容器打交道,Shiro的原生会话管理依然可以让用户使用相同的 HTTPServletRequest 和 HttpSession 调用完成与原生会话的协作,而不需要重构这些代码。这是因为 Shiro 完整实现了 Servlet规范中的 Session 部分以在 Web 应用中支持原生会话,因此开发者的所有 HTTPSession 的调用都会被委托给 Shiro 内部的原生会话 API。
六、附加特性
Apache Shiro 框架还包含有对保护 Java 应用非常有用的其他特性,如:
- 为维持跨线程的 Suject 提供了线程和并发支持(支持 Executor 和 ExecutorService);
- 为了将执行逻辑作为一种特殊的 Subject,支持 Callable 和 Runnable 接口;
- 为了表现为另一个 Subject 的身份,支持“Run As”(比如,在管理应用中有用);
- 支持测试工具,这样可以很容易的对 Shiro 的安全代码进行单元测试和集成测试。
七、框架的局限
如下是 Shiro 还未解决,但是值得知道的:
- 虚拟机级别的问题:Apache Shiro 当前还未处理虚拟机级别的安全,比如基于访问控制策略,阻止类加载器中装入某个类。然而,Shiro 集成现有的 JVM 安全操作并非白日做梦 - 只是没人给项目贡献这方面的工作。
- 多阶段认证:目前,Shiro 不支持“多阶段”认证,即用户可能通过一种机制登录,当被要求再次登录时,使用另一种机制登录。这在基于 Shiro 的应用中已经实现,但是通过应用预先收集所有必需信息再跟 Shiro 交互。这个功能在 Shiro 的未来版本中非常有可能得到支持。
- Realm 写操作:目前所有 Realm 实现都支持“读”操作来获取验证和授权数据以执行登录和访问控制。诸如创建用户帐户、组和角色或与用户相关的角色组和权限这类“写”操作还不支持。这是因为支持这些操作的应用数据模型变化太大,很难为所有的 Shiro 用户强制定义“写”API。
八、总结
Apache Shiro 是一个功能齐全、健壮、通用的 Java 安全框架,你可以用其为你的应用护航。通过简化应用安全的四个领域,即认证、授权、会话管理和加密,在真实应用中,应用安全能更容易被理解和实现。Shiro 的简单架构和兼容 JavaBean 使其几乎能够在任何环境下配置和使用。附加的 Web 支持和辅助功能,比如多线程和测试支持,让这个框架为应用安全提供了“一站式”服务。
鸣谢:
《让 Apache Shiro 保护你的应用》