摘自Application Security With Apache Shiro,这也是Shiro项目官网的介绍链接
什么是Apache Shiro?
Shiro是个安全框架,主要功能包括认证、授权、加密和session管理。
应用安全的四大基石(Shiro发力的方向)
- 认证(authentication):用户身份验证(登录)
- 授权(authorization):访问控制
- 加密(cryptography):保护、隐藏数据
- session管理(session management):每个用户的时间敏感的状态
Shiro的其它特性还包括web应用安全,单元测试,多线程支持(对上述特性的加强)
是什么催生了Shiro?
Shiro的发展历程:
2008年加入Apache项目之前已经存在了5年(JSecurity),在2003年,对于Java应用开发者,没有很多通用的安全框架替代品。当时应用的是JAAS(Java Authentication and Authorization Service),但是它存在太多缺点(认证还可以,但是授权很难用),并且JAAS深度绑定在虚拟机级别的安全特性(例如决定一个类是否应该允许被加载进JVM),作为应用开发人员,不想考虑这些问题。当时的session处理机制,只有HttpSessions,它需要一个web容器(或EJB2.1 Stateful Session Beans),即需要EJB容器,而一个和容器解耦的、能在所有环境使用的session管理机制才是开发人员想要的。加密,数据加密是一个很常见的需求,但是Java Cryptography Architecture非常难理解(除非是密码学专家),它的API充满了受检异常,而且很难用。而开发人员只需要简单的加密和解密的工具
在2003年的(应用)安全环境下看,当时没有任何一个单一且统一的框架可以同时包含开发人员所有的安全需求。因此JSecurity(Shiro)出生了
在今天,为什么要使用Shiro?
这个框架的蓝图(landscape)从2003年开始已经变了很多,所以现在仍然有很多理由使用Shiro
- 好用(easy to use):应用的安全性永远是又难做又必不可少的一个需求。如果能把这个过程简化,那么会减少很多痛苦
- 全能(comprehensive):安全问题的一站式解决方案
- 灵活(flexible):Shiro可以在所有应用环境中使用,虽然它在web环境下工作,但是对EJB、IoC的环境都不需要,甚至Shiro都没有太多的依赖
- 强Web(Web Capable):web应用支持,可以基于应用URL和web协议来创建灵活的安全协议(policies),也提供了一系列的JSP库来控制页面输出
- 可插拔的(pluggable):Shiro的clean API和设计模式使得它很容易和其它应用和框架合成(无缝接入Spring、Grails、Wicket、Tapestry、Mule、Apache Camel、Vaadin和等等等)
- 团队支持(supported):Apache项目基金会的项目。它的背后是开发者和用户的一个社群,有足够的技术支持
谁正在用Shiro?
Shiro和它的前身(JSecurity)在变成Apache项目基金会顶级项目之后已经在各种规模的公司项目使用。上面兼容的那些项目很多都正在使用Shiro。很多商业公司(Katasoft、New York commercial bank)也在使用Shiro
Shiro的架构包含三个主要概念:Subject SecurityManager Realm
Subject
当我们想让自己的应用变得安全,那么最相关的问题会浮现出来(当前用户是谁?当前用户能干什么?)。所以我们能想到的最自然的提高应用安全性的方法就是基于当前的用户来做。Shiro的API用Subject概念来表达这种想法
Subject在安全术语中意思是“当前正在作为的用户”。不单说“User”是因为这个词的隐含意义是人类。在安全世界中,Subject可以是人类,也可以是第三方进程,守护账户(daemon account)等等。简单来说就是目前正在和软件交互的事物(the thing)
Subject currentUser = SecurityUtils.getSubject();
获取到Subject对象后,可以直接访问你希望通过Shiro来控制当前用户的90%的行为(登录、登出、访问它们的session、执行授权检查等等),这里的关键点(key point)是Shiro的API是教导式(intuitive),因为它反映了开发者通常认为的”每个用户“的安全控制,使用Subject很容易,即在任何需要安全相关行为的地方都可以使用
SecurityManager
Subject的幕后伙伴是(behind the scenes)SecurityManager。Subject只代表当前用户的安全操作,但是SecurityManager管理的是所有用户的安全操作。这是Shiro架构的核心,并且它的行为就像是一系列连接着很多内部的安全组件的“umbrella”对象,最终形成了object graph。然而,只要SecurityManager和它的内部object graph被配置好,那么我们可以忽略它。大部分时间基本都是和Subject API打交道
根据应用的具体情况来设置SecurityManager。例如,一个web应用通常要在web.xml指定一个Shiro Servlet Filter,这会设置一个SecurityManager实例。如果运行的是一个单机应用,那么需要另一种方式来配置它。但是有很多的配置选项
每个应用都只有一个SecurityManager实例。像几乎Shiro中的一切,默认的SecurityManager实现是POJO,并且通过所有POJO兼容的配置方式来配置(Java代码,Spring XML,YAML,properties文件,ini文件等等)。基本上所有可以实例化类和调用JavaBeans兼容方法的事情都可以做(不大了解)
POJO是“plain old java object”,即它是一个纯粹的数据结构(字段、getter、可能有的setter),可能有从Object继承下来的方法或一些其它的接口(Serializable),但是本身没有其它行为。POJO是一个普通的Java对象,不被任何特殊限制绑定,也不需要类路径。Java Bean是一个规范,它需要一个被序列化的Java类,有一个无参构造器和每个字段的getter和setter。因此一个bean是一个POJO,但是POJO不一定是一个bean(有些工具(例如spring)不那么严格区分)
Shiro提供了默认的公分母(common denominator)方案(基于文本的INI配置)。INI易读易用,需要非常少的依赖。只要对object graph navigation有简单的了解,INI就可以高效配置简单的object graph(例如SecurityManager)
[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
[main]小结是配置SecurityManager对象和/或者任何SecurityManager使用的对象(Realm),这个例子中,有两个对象被配置
- cm对象,它是Shiro的HashedCredentialsMatcher类的一个实例。cm实例的很多属性通过点操作符来配置。在后续的代码中,IniSecurityManagerFactory的使用习惯,来表示object graph navigation和属性设置
- iniRealm对象是SecurityManager使用的组件来表示定义在INI格式中的用户账户
[user]小结,这里可以指定一个静态的用户账户列表,在简单的应用和测试中很方便
更详细的配置信息参考文档,应该会另开一篇
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);
上述代码展示了创建并设置一个SecurityManager的过程
- 加载INI配置(配置SecurityManager和它的构成组件)
- 基于配置创建SecurityManager实例(工厂模式)
- 让SecurityManager单例可以被应用访问。这里将它设置为VM-static单例,其实应用配置机制可以决定是否需要使用静态内存
Realms
一个Realm的行为像是一个在Shiro和应用的安全数据之间的桥梁或“connector”。在要和用户账户这种安全相关数据交互(login/access contorl)的时候,Shiro从应用配置的一个或多个Realms中来寻找这些事情。这个意义上,一个Realm其实就是一个专为安全而做的DAO:它处理数据源的连接详情,让Shiro可以看到相关的数据。当Shiro提供开箱即用(out-of-the-box)的Realms来连接一系列的安全数据源(directories)例如LDAP、关系型数据库(JDBC),文本配置源(INI properties文件等等)
Lightweight Directory Access Protocol (LDAP)是一个目录服务协议(directory service protocol),运行在TCP/IP斩上,它提供了一种用来连接、搜索和修改互联网目录的机制。这个服务是基于client-server模型,它的功能是允许访问存在的目录。LDAP的数据模型类似于 X.500 OSI directory service(不扩展了,看不懂),但是需要的资源更少。LDAP API简化了写互联网目录服务应用的过程
你可以将自己的Realm实现来嵌入到Shiro来表示自定义的数据源,如果默认的Realms不能支持需求
# realm配置,连接LDAP用户数据存储
[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
开始将框架应用起来
Authentication
认证是验证用户身份的过程,当一个用户通过了应用的认证,那么他们证明了他们确实是他们声称自己是的人(login过程)。这里是经典三步走
- 收集用户的验证信息(principals)和身份的支持性信息(credentials)
- 提交principal和credential给系统
- 如果提交的credential匹配了系统对principal的预期值,那么用户被认为可以给予认证。如果不通过,那么认证失败
Shiro实现用户名(principal)密码(password)登录
//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);
当login方法被调用,SecurityManager会接收到AuthenticationToken,然后将它分发给一个或多个配置的Realms,让他们去进行所需的认证检查。每个Realm都可以和提交的AuthenticationTokens做交互。通过Shiro的运行时AuthenticationException可以处理认证失败的情况
try {
currentUser.login(token);
}
catch (IncorrectCredentialsException ice) { …}
catch (LockedAccountException lae) { …}
…
catch (AuthenticationException ae) {…}
可以通过捕获AuthenticationException的子类来分别响应,也可以统一处理AuthenticationException
登录成功不代表用户可以做所有的事情,那么下一个问题:怎么控制用户可以做什么、不可以做什么?决定这个问题的过程是Authorization
Authorization
大多数用户实现访问控制是通过角色和权限(role/permission)。一个用户能做什么取决于他的角色和这个角色所具备的权限。Subject API可以很容易地体现这个过程
// 检查一个Subject是否已经被分配了特定角色
if ( subject.hasRole(“administrator”) ) {
//show the ‘Create User’ button
} else {
//grey-out the button?
}
使用角色来管理权限的缺点是,运行时不能改变角色(即角色是硬编码)。Shiro支持permission的概念。一个permission是功能的原始声明(开门、新建一个博客、删除用户xx),使用permission来反应应用最原本的功能,在改变应用功能时,只需要改变permission检查的部分即可。并且可以将permission分配给角色或用户(运行时)
// 通过检查permission替代检查role
if ( subject.isPermitted(“user:create”) ) {
//show the ‘Create User’ button
} else {
//grey-out the button?
}
“user:create” 字符串是一个permission字符串的例子,它绑定特殊的转换(parsing)习惯。Shiro通过WildcardPermission支持这种转换。它在创建安全协议时非常灵活,甚至支持实例级别(instance-level)的访问控制
if ( subject.isPermitted(“user:delete:jsmith”) ) {
//delete the ‘jsmith’ user
} else {
//don’t delete ‘jsmith’
}
这个例子展示了控制的粒度可以非常细,访问到单独的资源。也可以创建自己的permission语法。参考Shiro的Permission文档。就像认证过程,上述的调用最终是SecurityManager来处理,它会询问一个或多个Realms来作出访问控制的决策
Session Management
Shiro的高级特性,一个一致的(consistent)Session API,可以在任何应用、框架内使用。Shiro允许一个Session编程范式应用于所有的应用--从小的后台独立应用到大的集群化web应用。这样可以让开发人员不通过Servlet和EJB(如果他们不需要)的情况下来使用session。Shiro的session最重要的点在于它是容器独立的。Shiro的架构允许可插拔的Session数据存储,例如enterprise caches、关系型数据库、NoSQL系统等。这代表你可以配置session集群一次,之后无论部署环境有什么变化(不同容器),它的工作方式都一样。另一个优点是session数据可以在客户端共享(Swing桌面客户端可以加入到同一个web应用session中,如果终端用户是同时使用这两个终端,那么共享session很有用)
// 访问Subject的session
// 返回Subject存在的session,如果没有就新建一个返回
Session session = subject.getSession();
// 参数决定是否创建一个新session(如果没有现存的)
Session session = subject.getSession(boolean create);
一旦获取到一个session,那么可以像使用HttpSession
一样来使用它。Shiro团队认为HttpSession
的API很适合Java开发者,所以也保留了API的风格。它们之间最大的区别是,可以在任何应用中使用Shiro的session,而不只是web应用
Session session = subject.getSession();
session.getAttribute(“key”, someValue);
Date start = session.getStartTimestamp();
Date timestamp = session.getLastAccessTime();
session.setTimeout(millis);
...
Cryptography
加密是将数据隐藏或者模糊数据使得肉眼无法理解其具体内容。Shiro在加密方面的目标是简化并且让JDK的加密支持变得更可用。并且这个特性的API不是Subject-specific。可以在任何需要加密的地方使用Shiro的加密(可以没有Subject)。Shiro在加密支持关注的地方在于加密hash的空间(the areas of cryptographic hashes/message digests)和加密密码(cryptographic ciphers)
Hashing
JDK的MessageDigest
类很难用,它有一个静态方法(基于工厂模式的)API,而不是一个面向对象的,然后必须捕获受检异常(其实根本不需要捕获/处理)。如果需要16进制编码或者Base64编码的message digest输出,还得自己写(没有JDK官方支持)
Shiro通过一个clean and intuitive hashing API来解决问题。例如,MD5-hashing一个文件,得到这个值的16进制数,调用checksum
(提供文件下载时使用,用户将自己的MD5值和要下载文件的MD5值做对比),没有Shiro怎么做这件事?
- 将文件转为字节数组,JDK中没有API辅助做这件事,所以要自己写helper方法(开启FileInputStream,使用byte buffer,抛出异常)
- 使用
MessageDigest
类,将字符数组hash,处理好异常 - 编码hash过的字节数组成16进制。JDK中也没有API做这件事,所以要自己写另外一个helper方法(使用位操作和位转换)
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();
String encodedPassword = new Sha512Hash(password, salt, count).toBase64();
Ciphers
cipher是一种算法(使用key来反向转换数据)。使用它保证数据安全(传输和存储数据过程,数据是安全敏感的)。JDK的javax.crypto.Cipher
提供了这方面的支持,然而也是巨难用(没用过)。对于初学者,每个Cipher配置都由一个javax.crypto.Cipher
的实例来表示。下面一大段都在描述这个类有多难用,就不写了(😄)
CipherService
API来简化整个cryptographic ciphers概念。简单、无状态、线程安全的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);
可以直接实例化CipherService
对象,没有奇怪的工厂方法。Cipher配置选项使用JavaBean,而不是难以理解的`transformation string"。加密解密只需要简单的方法调用,没有强制受检异常。其它的优点:支持基于字节数组的加密/解密(“block“操作?)和基于流的加密/解密(音频、视频)
Web Support
Shiro的web支持,保护web应用的安全。对于web应用来说,Shiro设置很简单,唯一必要的事情是在web.xml中定义一个Shiro Servlet Filter
<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>
这个filter可以读取到前面提到的shiro.ini配置,所以无论部署环境有什么变化,配置信息都是一致的。一旦配置好,Shiro Filter会过滤所有的请求,保证指定请求的Subject在请求过程中是可以访问的。因为它过滤所有请求,所以在这里可以编写逻辑来保证只有满足某些条件的请求才允许通过
URL-Specific Filter Chains
Shiro通过URL filter chaining来支持security-specific过滤规则。允许对任何匹配的URL模式指定特别的filter chain。即使用Shiro的过滤机制执行安全规则有很大的灵活性
[urls]
/assets/** = anon
/user/signup = anon
/user/** = user
/rpc/rest/** = perms[rpc:invoke], authc
/** = authc
每行等号左边的值是web应用的路径,右边定义了filter chain(有序的,逗号分隔的Servlet filter列表)。每个filter都是一个普通的Servlet Filter,但是这些filter的名字是特殊的安全关联的过滤器(Shiro提供的开箱即用的),可以混合使用它们来创建自定义的安全规则
如果愿意,可以在web.xml中只定义Shiro Filter,并且在shiro.ini中定义所有其它的filter和filter chain,这是一个更简明且易于理解的filter chain定义
JSP Tag Library
Shiro提供了JSP tag库,可以基于当前Subject的状态控制JSP页面的输出
<%@ 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>
Web Session Management
Default Http Sessions
对于web应用,Shiro默认它的session基础使用已有使用过的的Servlet Container Session
Shiro’s Native Sessions in the Web Tier
如果需要Shiro的enterprise session特性(容器独立的集群(clustering)),那么你需要在web应用中开启Shiro的native session management。Shiro完全实现了Servlet规范中的Session部分来支持web应用中的native session。这代表无论何时调用HttpServletRequest或HttpSession的方法,Shiro会将其指向自己的内部native Session API
PS:真要能用到再看文档吧,并不理解这个clustering的意思
Additional Features
线程和并行的支持(跨线程维护Subject),Executor和ExcutorService支持
Callable和Runnable支持(执行指定Subject的逻辑)
“Run As”支持(另一个Subject的认证--管理员权限)
Shiro代码的单元测试和集成测试完美支持
Framework Limitations
VM级别的考量:Shiro现在不处理虚拟机级别的安全问题(阻止特定类从类加载器(根据访问控制原则)中载入)。其实也考虑过将Shiro继承到现有的JVM安全操作中(文中说没人做这件事)
Multi-Stage认证:Shiro现在不原生支持“multi-stage”认证(用户通过一种机制登录,只有当他通过另一种机制再次登录才会被询问),这个问题可以在应用层面解决。原生解决方案可能会在以后的版本中加入
Realm写操作:所有的Realm的实现都支持“read”操作,来获取认证和授权数据。“write”操作在应用之间的定义变化很大,所以很难去定义“write”的API
Upcoming Features
- Cleaner Web Filter机制,在不继承的情况下实现可插拔的过滤支持
- 更多的可插拔的Realm实现(倾向于组合而不是继承)
- 强健的OpenID和OAuth(可能的Hybrid)客户端支持
- Captcha支持
- 100%无状态应用(REST环境)的简易设置
- Multi-stage认证(通过request/response协议)
- 通过一个AuthorizationRequest来进行粗粒度的授权
- ANTLR语法(安全断言查询)security assertion queries (e.g. (‘role(admin) && (guest || !group(developer))’) -- 又看不懂了,ANTLR语法有时间可以看看
Summary(主要是夸夸夸)
Shiro是特性多、强健、通用的Java安全框架。简化了应用安全领域的四个方面:认证、授权、session管理、cryptography。在真正的应用中,安全更容易理解并且实现。Shiro简单的架构和JavaBean的兼容性使得它可以在任何环境下配置使用。对web的支持和高级特性补足了安全框架的短板(应用安全的一站式解决方案)。未来可期!