Apache Shiro 配置
Shiro设计的初衷就是可以运行于任何环境:无论是简单的命令行应用程序还是复杂的企业集群应用。由于运行环境的多样性,所以有多种配置机制可用于配置,本节我们将介绍Shiro内核支持的这几种配置机制。
许多配置选项
Shiro的SecurityManager实现和所有支持组件都与JavaBeans兼容。这使Shiro可以使用几乎任何配置格式进行配置,例如常规Java,XML(Spring,JBoss,Guice等),YAML,JSON,Groovy Builder标记等。
程序配置
最简单的创建并且使用SecurityManager的方式就是直接在代码中创建·org.apache.shiro.mgt.DefaultSecurityManager·类实例,比如:
1 | Realm realm = //instantiate or acquire a Realm instance. We'll discuss Realms later. |
只需区区三行代码,我们就已经为任何类型的应用程序配置好了一个全功能的Shiro运行环境,你看,多简单。
SecurityManager对象图谱:
就像我们在架构一节中介绍的,SecurityManager的实现是模块化的,而且可以兼容JavaBean,所以你可以通过setter和getter方法来配置SecurityManager及其内部组件。
比如如果你想把一个自定义的SessionDAO配置为SecurityManager的Session管理器,你可以直接调用SessionManager的setSessionDAO方法。
1 | ... |
你可以通过这种调用setter方法的方式来设置SecurityManager的任何内置组件。但是对于现实的应用程序来说,这不是一种理想的配置方式。主要有以下几点原因:
- 这种直接编码的方式要求我们知道这个具体的实现类在哪,并且要自己去创建他。而我们一般建议是依赖于抽象而不是具体,所以最好不要让我知道他具体的实现在哪里。
- 由于java的类型安全特性,当我们通过getter方法获取到某个类的具体实现之后,我们将不得不把他们强制类型转换为具体的类型,如此多的强制类型转换太丑了,不是一种好的编程实践。
- 如果我们通过SecurityUtils.setSecurityManager方法为当前的应用设置一个虚拟机范围内的静态SecurityManager对象,在大多数应用中都是ok的。但是如果我们要在一个虚拟机上运行多个使用Shiro的应用程序时,就可能会出乱子了。所以如果能够为每个应用程序创建一个的单例就更好了;
- 每次你要修改一下Shiro的配置都不得不重新编译程序;
虽然有以上提到的种种缺点,但是如果你要在一个内存受限的环境(比如智能手机)中使用Shiro,使用基于java代码的配置还是不错的选择。而如果内存不太受限的话,使用推荐使用基于文本的配置,因为他对用户更友好,具有更好的可读性。
INI配置
为了让这个文本配置方案能够在所有的开发环境中使用,并且尽可能的减少对于第三方工具的依赖,我们选择了INI格式来配置SecurityManager及其相关组件。INI具有易读、易配置的特性,可以适用于绝大多数的应用。
从INI文件中创建一个SecurityManager
以下将提供两种基于INI配置文件创建SecurityManager的方法。
从INI资源文件中创建SecurityManager
我们可以通过一个INI资源的路径来创建一个SecurityManager,资源可以通过文件系统、classpath、或者url中获取,不同的获取方式需要在资源路径前加不同的前缀,分别是file:, classpath 或者 url:,下面这个例子我们使用一个工厂类从根classpath中找到shiro.ini文件,然后实例化了一个SecurityManager对象。
1 |
|
从INI类实例中创建SecurityManager
如果有需要,我们也可以通过org.apache.shiro.config.Ini
类来做INI配置,这个Ini类的API和java.util.Properties类比较像,只是在接口中需要传入Section的名称。
例如:
1 |
|
以上我们已经知道如何通过INI配置来实例化SecurityManager对象,下面就让我们来看看一个真实的Shiro的配置文件到底长什么样。
INI Secions
所谓的INI文件其实就是按Section分隔的键值对的集合,不同的Section名称不同,每个Section内的键名称要有唯一性。每个Section可以看做就是一个Properties。
注释行可以井号(#) 开头,也可以 分号(;)开头。
下面就是一个Shiro能够解析的INI文件的例子,他的这些Section名称是Shiro所支持的。
1 | # ======================= |
[main]
[main]段主要用于配置SecurityManager及其他的依赖项,比如Realms。
要使用INI这种简单键值对文件格式来配置SecurityManager对象及其依赖项这种层级关系感觉难度有点大,只要稍微加上一些约定,你会发现INI文件能做的远比我们想象的要多,我们把这种基于INI的配置叫做“穷人”的依赖注入。虽然没有Spring/JBoss之类的高富帅那么强大,但是已经足够满足Shiro的配置要求了。下面是一个[main]段的配置例子,我们会在下文详细解释,不过我估计在解释之前,你也能猜个八九不离十了。
1 | [main] |
定义一个对象
看下面这段[main]配置片段。
1 | [main] |
这段配置创建了类 com.company.shiro.realm.MyRealm
的一个实例,命名为myRealm。如果这个类实现了org.apache.shiro.util.Nameable接口,则程序会使用参数”myRealm”来调用Nameable.setName接口。
设置对象属性
基本类型
基本类型属性可以像下面这样直接赋值
1 | ... |
这段配置翻译成Java代码后是这样的:
1 | ... |
这是如何做到的呢? 这里假定所有的对象都是和Java Bean兼容的POJO对象。·
在这种约定的前提下,当给对象设置属性时,Shiro会将所有的脏活、累活都交给Apache Common BeanUtils来干,虽然我们在INI文件内配置的是文本,但是BeanUtils知道如何将一个字符串的值转换为基本类型,并且调用该对象的对应的setter方法来给该POJO设置属性。
引用类型
如果要设置是引用类型怎么办?你可以用一个美元符号($)来引用前文中定义的对象,像下面这样,就这么简单。
1 | ... |
这只是找到名称为sha256Matcher定义的对象,然后使用BeanUtils在myRealm实例上设置该对象(通过调用myRealm.setCredentialsMatcher(sha256Matcher)
方法)。
嵌套属性
你可以像下面这样引用嵌套属性,给属性赋值,不管有多少个层级,都可以这么用。
使用INI线的等号左侧的虚线表示法,可以遍历对象图以到达要设置的最终对象/属性。例如,此配置行:
1 | ... |
BeanUtils会把他翻译成下面这样的代码:
1 | securityManager.getSessionManager().setGlobalSessionTimeout(1800000); |
这种嵌套的层级可以要多深有多深,比如:object.property1.property2.....propertyN.value=blah
BeanUtil支持的属性设置
只要是BeanUtil支持的属性设置方式你都可以在Shiro配置文件的[main] Section中配置。包括对于set/list/map属性的设置。详情可以参考 Apache Commons BeanUtils Website官方文档。
字节数组
因为在文本文件中无法直接表示二进制树,所以必须使用一种可以使用文本编码的方式来表示二进制数组,有两种选择,一种是:BASE64,一种是十六进制字符串。默认使用BASE64编码,因为表示相同长度的二进制,BASE64需要的字节更少。这显然更合适文本配置。
1 | # The 'cipherKey' attribute is a byte array. By default, text values |
十六进制的文本当然也是可以的,你要记得在文本的前面加上0x
。
1 | securityManager.rememberMeManager.cipherKey = 0x3707344A4093822299F31D008 |
集合属性
我们可以像设置其他属性一样去设置list、set、map类型的属性,不管是直接的属性还是嵌套属性。对于list和set,我们可以直接使用逗号分隔的值(或者引用)来设置,例如:
1 | sessionListener1 = com.company.my.SessionListenerImplementation |
对于map,你可以指定一系列都好分隔的键值对。键值对内部使用 冒号来作为键和值的分隔符,例如:
1 | object1 = com.company.some.Class |
你还可以直接使用对象来作为key,如下:
1 | anObject.map = $objectKey1:$objectValue1, $objectKey2:$objectValue2 |
注意事项
订单事项
上面的INI格式和约定非常方便且易于理解,但是不如其他基于文本/ XML的配置机制强大。使用上述机制时,最重要的要了解的是订单事项!
顺序问题
在INI文件中配置的顺序决定了他们翻译成Java代码之后的顺序,这块要小心。
重写实例
后定义的同名对象会覆盖之前定义的对象,如下第二个myRealm会覆盖掉第一个myRealm的定义,所以最终myRealm
是com.company.security.DatabaseRealm
的对象实例,而前一个定义的realm将永远不会被引用了。(自然也就被垃圾回收了)。
1 | ... |
默认的SecurityManager
你可能已经注意到,在上文的配置中,我们并没有创建SecurityManager对象就直接去设置他的嵌套属性了。
1 | myRealm = ... |
SecurityManager是特例,系统已经自动为你创建好了一个SecurityManager对象,你就只管用就好了。当然,如果你说我一定要使用字节实现的一个SecurityManager,那也不是不行,直接像下面这么写就可以了,就像 重写实例 章节中说的,这样就会覆盖掉系统自动创建的SecurityManager对象了。
1 | ... |
当然,这种情况很少发生。因为Shiro默认提供的SecurityManager具有很好的定制性,你几乎可以为配置任何属性。所以如果你发现自己要写一个自定义的SecurityManager的时候,不妨先问问自己:这真的有必要吗?
[users]
[users]Setion允许你定义一组用户账号。在那些只需要很少的用户账号,或者是那些不需要在运行时动态创建用户账号的运行环境下,这个功能很实用。你可以像下面这么写。
1 | [users] |
自动生成的IniRealm
一旦Shiro发现INI文件的[users], [roles]章节不为空,他就会自动创建一个org.apache.shiro.realm.text.IniRealm类的实例并命名为iniRealm,所以你可以在[main]章节中像配置其他对象一样给iniRealm配置属性。
行格式
在[users]的每一行将定义成如下格式
username=password, roleName1, roleName2, …, roleNameN
- 左边的key是用户名;
- 右边是逗号分隔的密码和角色,其中第一个而是密码,后续的是角色名,角色可以有多个;
- 角色是可选的;
密码加密
如果你不想让密码直接用明文显示,也可以使用任何你喜欢的加密算法(MD5,Sha1,Sha256, 等等)来对密码做加密,然后把加密之后的文本复制到INI文件中。加密之后的密码默认应该以十六进制的文本,当然也可以是BASE64的,具体看下问的说明。
简单安全的密码
对密码做加密的最佳实践是使用Shiro提供的 Command Line Hasher工具,他会对密码或者其他你想要加密的文本做哈希,这个工具对于要放在Shiro的INI配置文件的[users]中显示的密码最为实用。
如果你对密码做了加密,那么你就要告诉Shiro你对密码做了加密,不然他就不认识了。你可以在[main]端中配置Shiro隐式生成的iniRealm类,把你加密密密时使用的加密算法指定给credentialsMatcher
属性即可。
1 | [main] |
和其他属性一样,你可以给CredentialMatcher配置任何值来体现你的哈希策略,比如指定一个盐值,或者是哈希迭代的次数。要想更好的了解哈希策略,你可以查看org.apache.shiro.authc.credential.HashedCredentialsMatcher
的API文档。
比如,如果用户的密码是BASE64编码,而不是默认的16进制,则应该像下面这么配置。
1 | [main] |
[roles]
这个session用于定义角色与权限的对照关系,同样的,这种配置方式对于那些只有少数几种角色,并且不需要在运行时动态创建角色的应用程序特别实用。
1 | [roles] |
行格式
在[roles]中的每一行都必须定义为角色与权限的键值对关系,格式如下:
rolename=permissionDefinition1,permissionDefinition2,…, permissionDefinitionN
此处permissionDefinition 可以是任意文本,不过一般我们会建议使用和org.apache.shiro.authz.permission.WildcardPermission
格式兼容的文本格式,这种格式简单而又灵活。可以查看权限( Permissions)章节来了解更多关于这种权限格式的信息。
[urls]
这个Section的描述将放在 Web 章节。