在最高概念层面,Shiro的架构包含了3个主要概念:Subject, SecurityManager 和 Realms. 下面的图表是这些组件如何交互的高级概述,我们将覆盖下面的每一个概念:
"User”往往意味着一个人类,一个Subject可以是一个人,但它也可能代表一个第三方服务,后台账户,cron作业,或任何类似的东西-基本上是当前与软件交互的任何东西.
Subject 实例都绑定在SecurityManager上. 当你与Subject交互时,这些交互都会转换成特定subject与SecurityManager的交互.
SecurityManager 是Shiro架构的心脏,它扮演了一种保护伞(umbrella)对象,协调其内部的安全组件共同形成一个对象图.
后面我们会详细讨论SecurityManager, 但要意识到,当你与Subject交互时,其实是幕后的SecurityManager在执行Subject的安全操作.这可以从上面的图中得到反映.
Realms扮演的是Shiro与应用程序安全数据的桥梁或连接器.
Shiro提供了多个开箱即用的Realms来连接多种安全数据源,如LDAP, 关系型数据库(JDBC),像INI和属性文件这样的文本配置源.
下图展示了Shiro的核心架构概念以及每种概念的简短概述:
与软件进行交互的实体(用户,第三方服务,cron作业等等)特定安全视图.
正如上面提到的, SecurityManager是Shiro架构的心脏.它主要充当保护伞对象,协调其管理组件来确保工作顺利进行. 同时,它也会管理每个应用程序用户的Shiro视图,因此它知道如何执行每个用户上的操作.
认证负责验证用户的登录. 当一个用户试图登录时,认证将执行验证逻辑. 认证组件知道如何协调一个或多个存储相关用户信息的. 从Realms中获取的数据用于验证用户的身份,以确保其合法性.
如果配置了多个Realm, AuthenticationStrategy 会协调Realms来确定在哪些情况下认证是成功或是失败的(例如,如果某个realm是成功的,但其它是失败的l,那么认证是成功的吗?
是否需要所有realms都成功?还是只需要第一个成功?).
授权是在应用程序中负责确定访问控制的组件.最终说来它只是允许用户做什么或不做什么的机制.类似于Authenticator,Authorizer也知道如何协调多个后端数据源来访问角色和权限信息.
Authorizer可使用这些信息来决定一个用户是否允许来执行给定的操作.
SessionManager知道如何来创建和管理用户生命周期,以为所有环境中的用户提供强大的会话体验. 在安全框架的世界中,这是独特的特性- Shiro有管理任何环境中用户会话的能力, 即使这些环境处于非Web/Servlet或在EJB容器环境.
默认情况下,如果可行的话,Shiro会使用现有的会话机制, (例如. Servlet容器),但如果不存在这样的环境,如独立程序或非web环境,它会使用内置的企业会话管理来提供相同的编程体验. SessionDAO的存在允许使用任何数据源来存储会话.
SessionDAO可代表SessionManager来执行会话持久化操作(CRUD).这允许在会话管理框架中插入任何数据存储.
缓存管理器用于创建和管理其它Shiro组件使用的缓存实例生命周期.因为Shiro在执行认证,授权和会话管理时,可访问多个后端数据源,当使用这些数据源时,缓存通常是框架中提高性能的最好架构特征.
任何一个现代开源或企业级缓存产品都可以插入到Shiro中,以提供快速,高效的用户体验.
加密是企业级安全框架中一个很自然的功能. Shiro的加密包包含了易于使用和理解的加密 Ciphers, Hashes (aka digests)表现以及不同的编码实现.
包中的所有类都是精心设计为易于使用和理解. 任何使用过Java本地加密的人,都可以轻易地驾驭它.
Shiro的加密APIs简化了复杂的Java机制,使得加密对于普通人来说也能简单使用.
正如上面所提到的, Realms扮演的是Shiro与应用程序安全数据之间的桥梁或连接器.
当它与像用户帐户这样的相关安全数据进行实际交互时,如执行认证(login) 和授权(访问控制), Shiro会查看为应用程序中配置的一个或多个Realms.
你可以根据需要来配置多个Realms (通常情况下一个数据源一个Realms) ,Shiro会按照需要来为认证和授权协调数据.
SecurityManager
因为Shiro的API鼓励使用以Subject为中心的编程模型,大部分应用程序开发者会极少直接与SecurityManager交互(框架开发者有时可能会发现它很有用).
即使是这样,了解SecurityManager的功能,特别是在为应用配置时, 也是极其重要的.
设计
如前所述,应用程序安全管理器会为所有应用程序用户执行安全操作和管理用户状态. 在Shiro的默认SecurityManager实现中包含:
- Authentication(认证)
- Authorization(授权)
- Session Management(会话管理)
- Cache Management(缓存管理)
- Realm coordination(Realm协作)
- Event propagation(事件传播)
- "Remember Me" Services(记住我服务)
- Subject creation(Subject创建)
- Logout(登出)
等等.
但在单个组件中,管理着多个功能. 而且,如果一切功能都集中到单一的实现类中,会使这些操作变得灵活和可定制化是相当困难的.
为了简化配置和开启灵活的配置性/可插拔性, Shiro的实现设计上是高度模块化的- SecurityManager实现及其类层次并没有做这么多工作.
相反,SecurityManager实现几乎充当的是轻量级容器组件, 几乎把所有行为/操作都委派给了内嵌/包装组件.上面的详细框架图就反映了这种包装设计.
虽然是由组件来实际执行逻辑,SecurityManager实现知道如何以及何时来协调组件来完成正确行为.
SecurityManager 实现同时也是JavaBeans兼容的,这允许你通过标准的Java访问器/修改器方法(get*/set*)来轻松地定制可插拔组件.
这意味着Shiro的架构模块可以很容易配置自定义行为。
简单配置
由于JavaBeans的兼容性, 使其很容易地通过支持JavaBeans的自定义组件来配置SecurityManager,如Spring, Guice, JBoss等等. Apache Shiro 配置
Shiro设计为可工作于任何环境, 从简单的命令行应用程序到大型企业集群应用程序. 由于环境的多样性,存在着多种配置机制. 本章讲述Shiro核心支持的配置机制.
多个配置选项
Shiro的SecurityManager实现以及所有支持组件都是与JavaBeans兼容的.这允许Shiro使用特定配置格式,如常规Java, XML (Spring, JBoss, Guice, etc), YAML, JSON, Groovy Builder标记来配置. 编程配置
创建SecurityManager并使用可用的最简单方式是创建一个org.apache.shiro.mgt.DefaultSecurityManager. 例如:
Realm realm = //实例化或获取一个Realm实例.我们后面会讨论Realms. SecurityManager securityManager = new DefaultSecurityManager(realm); //通过静态内存,使SecurityManager 实例对整个应用程序可用: SecurityUtils.setSecurityManager(securityManager);
令人惊讶的是,虽只有3行代码,但你现在有了一个适合于许多应用且功能齐全的Shiro环境。多么容易!
SecurityManager 对象图
正如在
Architecture章节中讨论的, Shiro的SecurityManager实现本质上内嵌特定安全的组件模块对象图.
因为它们也是JavaBeans兼容的,你可调用任何内嵌组件上的 getter和setter方法来配置SecurityManager和它的内部对象图.
例如,如果你想配置SecurityManager实例来使用自定义的SessionDAO来定制
Session Management,你可以直接使用内置的SessionManager的setSessionDAO来设置SessionDAO:
...
DefaultSecurityManager securityManager = new DefaultSecurityManager(realm);
SessionDAO sessionDAO = new CustomSessionDAO();
((DefaultSessionManager)securityManager.getSessionManager()).setSessionDAO(sessionDAO); ...
使用直接方法调用,你可以配置SecurityManager的对象图的任何部分.
虽像程序化定制一样简单,但它并不代表大多数现实世界应用程序的理想配置。
有以下几个原因为什么编程配置可能不适合您的应用程序:
- 它需要你了解和实例化一个直接实现.如果不需要了解具体和查找位置,这是很好的.
- 由于Java的类型安全特性,你需要在get*方法中对特定实现进行类型转换. 因此太多的转换是丑陋的,冗长的,并紧密耦合了你的实现类.
- SecurityUtils.setSecurityManager 方法调用使得实例化了的SecurityManager实例化了一个 VM 静态单例, 在这里是很好的,但对于多数应用程序来说,如果在同一个JVM中运行了多个开启了Shiro的应用程序,将会引发问题.本身来说,如果实例是应用程序级单例的,这会更好,但不是静态内存引用.
- 为了让Shiro配置生效,每次你都需要重新编译代码.
但,即使有这些警告,直接编程操作方案对于内存受限的环境,仍然是价值的,像智能手机应用程序.如果你的应用程序不会运行内存受限环境中,你会发现基于文本的配置更加简单和阅读.
INI 配置
大部分应用程序都受益于基于文本的配置,这样可以独立于源码进行修改,甚至使得那于不熟悉Shiro API的人来说,也更易于理解.
为了确保通用基于文本配置的解决方案能工作于任何环境(有很小的依赖),Shiro支持使用
INI format 来构建SecurityManager 对象图和它的支撑组件.
INI易于阅读和配置,并可以简单建立而适用于大部分应用程序.
从INI创建SecurityManager
下面有两个例子来讲述如何基于INI配置来构建SecurityManager.
从INI资源来构建SecurityManager
我们可以从INI资源路径来创建SecurityManager实例. 资源可通过文件系统, classpath,或URLs来获取,此时其前辍分别为file:, classpath:, 或 url: .
这个例子使用Factory来从classpath根路径下来摄取shiro.ini文件,并返回SecurityManager实例:
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.util.Factory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.config.IniSecurityManagerFactory;
...
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
从INI实例来构建SecurityManager
For example:
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.util.Factory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.config.Ini;
import org.apache.shiro.config.IniSecurityManagerFactory;
...
Ini ini = new Ini(); //按需填充 Ini实例
...
Factory<SecurityManager> factory = new IniSecurityManagerFactory(ini);
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
现在,我们知道如何从INI配置来构建一个SecurityManager, 让我们明确地了解如何定义一个Shiro INI配置.
INI 分段
INI是一个包含由唯一分段组织的key/value对的文本配置. 每个段中Keys是唯一的,而不是整个配置(这一点不像JDK Properties). 每个段都可像单个Properties定义进行查看.
注释行可以#或;开头
下面是分段的例子:
# ======================= # Shiro INI configuration # =======================
[main]
# 这里用于定义对象及其属性,如securityManager, Realms 以及任何需要用于构建SecurityManager的配置
[users]
# 'users' 分段用于简单部署
# 当你只需要少量静态定义的用户帐户时
[roles]
# 'roles' 分段用于简单部署
# 当你只需要少量静态定义的角色时
[urls] # 'urls' 分段用于web应用程序中作基于url的基础安全控制
# 我们会Web文档中讨论这个分段
[main]
[main]分段是你配置应用程序SecurityManager实例,及其任何依赖,如
Realms.
像SecurityManager或其依赖这样的配置对象实例听起来对于INI来说,是一件困难的事情,因为在这里我们只能使用name/value对.
但是通过一点点的约定和对对象图的理解,你会发现你可以做得很好。Shiro用这些假设使一个简单的配置机制成为了可能。
我们常喜欢把这种方法称为“穷人的“依赖注入,虽然不比全面的Spring/ Guice / JBoss的XML文件强大,但你会发现它没有过多的复杂性。
当然也可使用其他的配置机制,但对于Shiro来说,他们不是必须的.
只是为了刺激你的欲望,这里有一个有效的[main]配置实例:
[main]
sha256Matcher = org.apache.shiro.authc.credential.Sha256CredentialsMatcher
myRealm = com.company.security.shiro.DatabaseRealm
myRealm.connectionTimeout = 30000
myRealm.username = jsmith
myRealm.password = secret
myRealm.credentialsMatcher = $sha256Matcher
securityManager.sessionManager.globalSessionTimeout = 1800000
定义对象
细想下面的[main] 分段片断:
[main] myRealm = com.company.shiro.realm.MyRealm ...
这行实例化了一个com.company.shiro.realm.MyRealm类型的新对象,并为了以后的引用和配置,使其在名称myRealm名称下可用.
如果实例的对象实现了org.apache.shiro.util.Nameable接口, 那么可在对象上使用名称值(myRealm)来调用Nameable.setName.
设置对象属性
原始类型值
简单原始类型属性可以使用=号来赋值:
...
myRealm.connectionTimeout = 30000
myRealm.username = jsmith ...
这些行的配置转换成方法调用为:
...
myRealm.setConnectionTimeout(30000);
myRealm.setUsername("jsmith"); ...
这怎么可能呢?它假定所有的对象都是java bean兼容的POJOs。
在后台,Shiro默认使用Apache Commons BeanUtils来设置这些属性。所以尽管INI值是文本,BeanUtils知道如何将字符串值的原始类型,然后调用相应的JavaBean的setter方法。
引用值
如果需要设置的值不是原始类型而是其它对象呢? 你可以使用$来引用先前定义的实例.例如:
...
sha256Matcher = org.apache.shiro.authc.credential.Sha256CredentialsMatcher
...
myRealm.credentialsMatcher = $sha256Matcher ...
这会简单地定位名为sha256Matcher 的对象,然后使用 BeanUtils 在 myRealm 实例上来设置这个对象(通过调用myRealm.setCredentialsMatcher(sha256Matcher)方法).
内嵌属性
在=的左边使用点号分隔法来对内嵌属性进行设值.例如:
...
securityManager.sessionManager.globalSessionTimeout = 1800000 ...
用BeanUtils转换成下面的逻辑为:
securityManager.getSessionManager().setGlobalSessionTimeout(1800000);
图的遍历可以根据需要来设置深度: object.property1.property2....propertyN.value = blah
BeanUtils Property Support
Any property assignment operation supported by the BeanUtils.setProperty method will work in Shiro's [main] section, including set/list/map element assignments. Byte 数组值
由于原始字节数组不能文本格式进行指定,我们必须使用字节数组的文本编码.这些值可以通过Base64 编码字符串(默认)或 Hex编码字符串进行指定.
默认是Base64 ,因为 Base64编码需要更少的实际文本来表现值- 它有一个更大的编码字母表,这意味着你的令牌更短(对于文本配置更好).
# The 'cipherKey' attribute is a byte array. By default, text values
# for all byte array properties are expected to be Base64 encoded:
securityManager.rememberMeManager.cipherKey = kPH+bIxk5D2deZiIxcaaaA== ...
然而,如果你选择使用Hex编码, 你必须使用0x ('zero' 'x')作为字符串的前辍:
securityManager.rememberMeManager.cipherKey = 0x3707344A4093822299F31D008
Collection属性
Lists, Sets 和Maps 也可以像其它属性来设置 -要么直接设置,要么通过内嵌属性设置. 对于sets和Lists,只需要以逗号分隔来指定值或对象引用的集合:
sessionListener1 = com.company.my.SessionListenerImplementation
...
sessionListener2 = com.company.my.other.SessionListenerImplementation
...
securityManager.sessionManager.sessionListeners = $sessionListener1, $sessionListener2
对于Maps,你可以以逗号分隔的key-value对列表来指定, 每个key-value对都由冒号来分隔
object1 = com.company.some.Class
object2 = com.company.another.Class
...
anObject = some.class.with.a.Map.property
anObject.mapProperty = key1:$object1, key2:$object2
In the above example, the object referenced by $object1 will be in the map under the String key key1, i.e. map.get("key1") returns object1. You can also use other objects as the keys:
anObject.map = $objectKey1:$objectValue1, $objectKey2:$objectValue2 ...
注意事项
顺序影响
虽然INI格式非常方便和易于理解,但它并没有像其它基于text/XML配置机制强大. 使用上述机制时,要理解的最重要的事情是顺序带来的影响.
小心
Each object instantiation and each value assignment is executed in the order they occur in the 在每个[main] 分段中,每个对象实例和及值的分配都是出现顺序来执行的.
这些行最终都会转换成对avaBeans getter/setter 方法的调用,且这些方法也是按相同顺序来调用的!在编写配置时,要当心.
覆盖实例
任何对象都可以通过配置文件后面新定义的实例来覆盖.因此对于下面的例子,第二个myRealm定义将覆盖前面一个:
...
myRealm = com.company.security.MyRealm
...
myRealm = com.company.security.DatabaseRealm ...
译者注:如果这两个key在同一个段内,就与先前的描述违背了.
这个导致myRealm成为com.company.security.DatabaseRealm的实例,先前的实例将不再使用(垃圾回收掉).
默认SecurityManager
你可能已经注意到了,上面的例子中没有定义 SecurityManager 实例, 我们跳到右边只设置了一个内嵌属性:
myRealm = ...
securityManager.sessionManager.globalSessionTimeout = 1800000 ...
这是因为securityManager实例是特定的一个- 它已经为你实例化好了,因此你不需要了解具体的SecurityManager实现.
当然,如果你想指定你自己的实现,你可以定义你的实现来进行覆盖:
... securityManager = com.company.security.shiro.MyCustomSecurityManager ...
当然,极少需要这样- Shiro的SecurityManager实现是可充分定制的,通常可以按任何需要进行配置.
[users]
[users] 分段允许你定义一些静态用户帐户集合. 这在一个非常小的用户帐户或用户帐户不需要在运行时动态创建环境中有用. 下面是例子:
[users]
admin = secret lonestarr = vespa, goodguy, schwartz darkhelmet = ludicrousspeed, badguy, schwartz
Automatic IniRealm
你可以像上面一样来配置其它任何对象.
行格式
[users]分段中的每行都有下面的格式:
username = password, roleName1, roleName2, ..., roleNameN
- 等号左边的值是用户名
- 等号右边的第一个值是用户的密码.密码是必须的.
- 密码后面以逗号分隔的值是为用户分配的角色.角色名称是可选的.
密码加密
如果你不想[users]分段中的密码是明文的,你可以使用你喜欢的hash算法(MD5, Sha1, Sha256)来对其加密,并使用结果字符串来作为密码值.
默认情况下期望的密码字符串是Hex编码的,但你可以使用Base64编码来代替(见下面).
Easy Secure Passwords
一旦你指定了哈稀化的文本值, 你必须告诉Shiro它们是加密的. 你可为隐式创建的iniRealm 在[main]分段中来配置适当的对应你指定的哈希算法的CredentialsMatcher实现:
[main]
...
sha256Matcher = org.apache.shiro.authc.credential.Sha256CredentialsMatcher
...
iniRealm.credentialsMatcher = $sha256Matcher
...
[users]
# user1 = sha256-hashed-hex-encoded password, role1, role2,
...
user1 = 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b, role1, role2, ...
你可在CredentialsMatcher上配置任何属性,如反映哈稀策略的对象,例如, 是否使用盐(salting)或者执行多少次hash迭代.
例如,如果你想对用户密码使用Base64编码来代替默认的Hex,你可以按如下来指定:
[main]
...
# true = hex, false = base64:
sha256Matcher.storedCredentialsHexEncoded = false
[roles]
再次说明,这对于少量角色或在运行时不需要动态创建角色的环境有用.下面是一个例子:
[roles]
# 'admin' 的角色拥有所有权限, 使用通配符'*'来表示 admin = *
# 'schwartz' 角色可在任何lightsaber上执行任何操作(*): schwartz = lightsaber:*
# 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with # license plate 'eagle5' (instance specific id) goodguy = winnebago:drive:eagle5
行格式
[roles]分段中的每行必须按下面的格式来定义role-to-permission(s) key/value:
rolename = permissionDefinition1, permissionDefinition2, ..., permissionDefinitionN
内部逗号
注意,如果在单个permissionDefinition 中存在逗号分隔符(如. printer:5thFloor:print,info),你必须使用双引号,以避免解析错误:
"printer:5thFloor:print,info"
无权限的角色
如果角色不需要关联权限,那么则不需要在[roles]分段中列出.如果角色不存在,只需要在[users]分段中定义角色就足够创建角色了.
[urls]