Apache Shiro 配置

Apache Shiro 配置

Shiro设计的初衷就是可以运行于任何环境:无论是简单的命令行应用程序还是复杂的企业集群应用。由于运行环境的多样性,所以有多种配置机制可用于配置,本节我们将介绍Shiro内核支持的这几种配置机制。

许多配置选项

Shiro的SecurityManager实现和所有支持组件都与JavaBeans兼容。这使Shiro可以使用几乎任何配置格式进行配置,例如常规Java,XML(Spring,JBoss,Guice等),YAML,JSON,Groovy Builder标记等。

程序配置

最简单的创建并且使用SecurityManager的方式就是直接在代码中创建·org.apache.shiro.mgt.DefaultSecurityManager·类实例,比如:

1
2
3
4
5
Realm realm = //instantiate or acquire a Realm instance.  We'll discuss Realms later.
SecurityManager securityManager = new DefaultSecurityManager(realm);

//Make the SecurityManager instance available to the entire application via static memory:
SecurityUtils.setSecurityManager(securityManager);

只需区区三行代码,我们就已经为任何类型的应用程序配置好了一个全功能的Shiro运行环境,你看,多简单。

SecurityManager对象图谱:

就像我们在架构一节中介绍的,SecurityManager的实现是模块化的,而且可以兼容JavaBean,所以你可以通过setter和getter方法来配置SecurityManager及其内部组件。

比如如果你想把一个自定义的SessionDAO配置为SecurityManager的Session管理器,你可以直接调用SessionManager的setSessionDAO方法。

1
2
3
4
5
6
7
8
...

DefaultSecurityManager securityManager = new DefaultSecurityManager(realm);

SessionDAO sessionDAO = new CustomSessionDAO();

((DefaultSessionManager)securityManager.getSessionManager()).setSessionDAO(sessionDAO);
...

你可以通过这种调用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
2
3
4
5
6
7
8
9
10
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

如果有需要,我们也可以通过org.apache.shiro.config.Ini类来做INI配置,这个Ini类的API和java.util.Properties类比较像,只是在接口中需要传入Section的名称。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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();
//populate the Ini instance as necessary
...
Factory<SecurityManager> factory = new IniSecurityManagerFactory(ini);
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);

以上我们已经知道如何通过INI配置来实例化SecurityManager对象,下面就让我们来看看一个真实的Shiro的配置文件到底长什么样。

INI Secions

所谓的INI文件其实就是按Section分隔的键值对的集合,不同的Section名称不同,每个Section内的键名称要有唯一性。每个Section可以看做就是一个Properties。

注释行可以井号(#) 开头,也可以 分号(;)开头。

下面就是一个Shiro能够解析的INI文件的例子,他的这些Section名称是Shiro所支持的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# =======================
# Shiro INI configuration
# =======================

[main]
# Objects and their properties are defined here,
# Such as the securityManager, Realms and anything
# else needed to build the SecurityManager

[users]
# The 'users' section is for simple deployments
# when you only need a small number of statically-defined
# set of User accounts.

[roles]
# The 'roles' section is for simple deployments
# when you only need a small number of statically-defined
# roles.

[urls]
# The 'urls' section is used for url-based security
# in web applications. We'll discuss this section in the
# Web documentation

[main]

[main]段主要用于配置SecurityManager及其他的依赖项,比如Realms。
要使用INI这种简单键值对文件格式来配置SecurityManager对象及其依赖项这种层级关系感觉难度有点大,只要稍微加上一些约定,你会发现INI文件能做的远比我们想象的要多,我们把这种基于INI的配置叫做“穷人”的依赖注入。虽然没有Spring/JBoss之类的高富帅那么强大,但是已经足够满足Shiro的配置要求了。下面是一个[main]段的配置例子,我们会在下文详细解释,不过我估计在解释之前,你也能猜个八九不离十了。

1
2
3
4
5
6
7
8
9
10
[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]配置片段。

1
2
3
[main]
myRealm = com.company.shiro.realm.MyRealm
...

这段配置创建了类 com.company.shiro.realm.MyRealm的一个实例,命名为myRealm。如果这个类实现了org.apache.shiro.util.Nameable接口,则程序会使用参数”myRealm”来调用Nameable.setName接口。

设置对象属性

基本类型

基本类型属性可以像下面这样直接赋值

1
2
3
4
...
myRealm.connectionTimeout = 30000
myRealm.username = jsmith
...

这段配置翻译成Java代码后是这样的:

1
2
3
4
...
myRealm.setConnectionTimeout(30000);
myRealm.setUsername("jsmith");
...

这是如何做到的呢? 这里假定所有的对象都是和Java Bean兼容的POJO对象。·

在这种约定的前提下,当给对象设置属性时,Shiro会将所有的脏活、累活都交给Apache Common BeanUtils来干,虽然我们在INI文件内配置的是文本,但是BeanUtils知道如何将一个字符串的值转换为基本类型,并且调用该对象的对应的setter方法来给该POJO设置属性。

引用类型

如果要设置是引用类型怎么办?你可以用一个美元符号($)来引用前文中定义的对象,像下面这样,就这么简单。

1
2
3
4
5
...
sha256Matcher = org.apache.shiro.authc.credential.Sha256CredentialsMatcher
...
myRealm.credentialsMatcher = $sha256Matcher
...

这只是找到名称为sha256Matcher定义的对象,然后使用BeanUtils在myRealm实例上设置该对象(通过调用myRealm.setCredentialsMatcher(sha256Matcher)方法)。

嵌套属性

你可以像下面这样引用嵌套属性,给属性赋值,不管有多少个层级,都可以这么用。

使用INI线的等号左侧的虚线表示法,可以遍历对象图以到达要设置的最终对象/属性。例如,此配置行:

1
2
3
...
securityManager.sessionManager.globalSessionTimeout = 1800000
...

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
2
3
4
5
# 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==
...

十六进制的文本当然也是可以的,你要记得在文本的前面加上0x

1
securityManager.rememberMeManager.cipherKey = 0x3707344A4093822299F31D008

集合属性

我们可以像设置其他属性一样去设置list、set、map类型的属性,不管是直接的属性还是嵌套属性。对于list和set,我们可以直接使用逗号分隔的值(或者引用)来设置,例如:

1
2
3
4
5
sessionListener1 = com.company.my.SessionListenerImplementation
...
sessionListener2 = com.company.my.other.SessionListenerImplementation
...
securityManager.sessionManager.sessionListeners = $sessionListener1, $sessionListener2

对于map,你可以指定一系列都好分隔的键值对。键值对内部使用 冒号来作为键和值的分隔符,例如:

1
2
3
4
5
6
object1 = com.company.some.Class
object2 = com.company.another.Class
...
anObject = some.class.with.a.Map.property

anObject.mapProperty = key1:$object1, key2:$object2

你还可以直接使用对象来作为key,如下:

1
anObject.map = $objectKey1:$objectValue1, $objectKey2:$objectValue2

注意事项

订单事项

上面的INI格式和约定非常方便且易于理解,但是不如其他基于文本/ XML的配置机制强大。使用上述机制时,最重要的要了解的是订单事项!

顺序问题

在INI文件中配置的顺序决定了他们翻译成Java代码之后的顺序,这块要小心。

重写实例

后定义的同名对象会覆盖之前定义的对象,如下第二个myRealm会覆盖掉第一个myRealm的定义,所以最终myRealmcom.company.security.DatabaseRealm的对象实例,而前一个定义的realm将永远不会被引用了。(自然也就被垃圾回收了)。

1
2
3
4
5
...
myRealm = com.company.security.MyRealm
...
myRealm = com.company.security.DatabaseRealm
...

默认的SecurityManager

你可能已经注意到,在上文的配置中,我们并没有创建SecurityManager对象就直接去设置他的嵌套属性了。

1
2
3
4
myRealm = ...

securityManager.sessionManager.globalSessionTimeout = 1800000
...

SecurityManager是特例,系统已经自动为你创建好了一个SecurityManager对象,你就只管用就好了。当然,如果你说我一定要使用字节实现的一个SecurityManager,那也不是不行,直接像下面这么写就可以了,就像 重写实例 章节中说的,这样就会覆盖掉系统自动创建的SecurityManager对象了。

1
2
3
...
securityManager = com.company.security.shiro.MyCustomSecurityManager
...

当然,这种情况很少发生。因为Shiro默认提供的SecurityManager具有很好的定制性,你几乎可以为配置任何属性。所以如果你发现自己要写一个自定义的SecurityManager的时候,不妨先问问自己:这真的有必要吗?

[users]

[users]Setion允许你定义一组用户账号。在那些只需要很少的用户账号,或者是那些不需要在运行时动态创建用户账号的运行环境下,这个功能很实用。你可以像下面这么写。

1
2
3
4
[users]
admin = secret
lonestarr = vespa, goodguy, schwartz
darkhelmet = ludicrousspeed, badguy, schwartz

自动生成的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
2
3
4
5
6
7
8
9
10
[main]
...
sha256Matcher = org.apache.shiro.authc.credential.Sha256CredentialsMatcher
...
iniRealm.credentialsMatcher = $sha256Matcher
...

[users]
# user1 = sha256-hashed-hex-encoded password, role1, role2, ...
user1 = 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b, role1, role2, ...

和其他属性一样,你可以给CredentialMatcher配置任何值来体现你的哈希策略,比如指定一个盐值,或者是哈希迭代的次数。要想更好的了解哈希策略,你可以查看org.apache.shiro.authc.credential.HashedCredentialsMatcher的API文档。

比如,如果用户的密码是BASE64编码,而不是默认的16进制,则应该像下面这么配置。

1
2
3
4
[main]
...
# true = hex, false = base64:
sha256Matcher.storedCredentialsHexEncoded = false

[roles]

这个session用于定义角色与权限的对照关系,同样的,这种配置方式对于那些只有少数几种角色,并且不需要在运行时动态创建角色的应用程序特别实用。

1
2
3
4
5
6
7
8
[roles]
# 'admin' role has all permissions, indicated by the wildcard '*'
admin = *
# The 'schwartz' role can do anything (*) with any lightsaber:
schwartz = lightsaber:*
# The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with
# license plate 'eagle5' (instance specific id)
goodguy = winnebago:drive:eagle5

行格式

在[roles]中的每一行都必须定义为角色与权限的键值对关系,格式如下:

rolename=permissionDefinition1,permissionDefinition2,…, permissionDefinitionN

此处permissionDefinition 可以是任意文本,不过一般我们会建议使用和org.apache.shiro.authz.permission.WildcardPermission格式兼容的文本格式,这种格式简单而又灵活。可以查看权限( Permissions)章节来了解更多关于这种权限格式的信息。

[urls]

这个Section的描述将放在 Web 章节。

0%