Apache Shiro系列之五,概述 —— 配置

时间:2022-09-08 04:12:07

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

 
    小贴士:多种配置方案:
    Shiro的SecurityManager是和JavaBean兼容的,所以我们可以使用诸如Java、Xml(Spring、Jboss、Guice等)、YAML、Json、Groovy等配置方式。
 
一、基于Java代码的配置
    最简单的创建并且使用SecurityManager的方式就是直接在代码中创建org.apache.shiro.mgt.DefaultSecurityManager类实例,比如:
 Realm realm =//instantiate or acquire a Realm instance. We'll discuss Realms later.
SecurityManager securityManager =newDefaultSecurityManager(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方法。
...
DefaultSecurityManager securityManager =newDefaultSecurityManager(realm);
SessionDAO sessionDAO =newCustomSessionDAO();
((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对象。
 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 =newIniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
    从INI类实例中创建SecurityManager
    如果有需要,我们也可以通过org.apache.shiro.config.Ini类来做INI配置,这个Ini类的API和java.util.Properties类比较像,只是在接口中需要传入Section的名称。
    例如:
 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 =newIni();
//populate the Ini instance as necessary
...
Factory<SecurityManager> factory =newIniSecurityManagerFactory(ini);
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
 
    以上我们已经知道如何通过INI配置来实例化SecurityManager对象,下面就让我们来看看一个真实的Shiro的配置文件到底长什么样。
 
    (二) INI Secions
    所谓的INI文件其实就是按Section分隔的键值对的集合,不同的Section名称不同,每个Section内的键名称要有唯一性。每个Section可以看做就是一个Properties。
    注释行可以井号(#) 开头,也可以 分号(;)开头。、
    下面就是一个Shiro能够解析的INI文件的例子,他的这些Section名称是Shiro所支持的。
#=======================
#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
  (1) [main]
[main]段主要用于配置SecurityManager及其他的依赖项,比如Realms。
要使用INI这种简单键值对文件格式来配置SecurityManager对象及其依赖项这种层级关系感觉难度有点大,只要稍微加上一些约定,你会发现INI文件能做的远比我们想象的要多,我们把这种基于INI的配置叫做“穷人”的依赖注入。虽然没有Spring/JBoss之类的高富帅那么强大,但是已经足够满足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
...
    这段配置翻译成Java代码后是这样的:    
 ...
myRealm.setConnectionTimeout(30000);
myRealm.setUsername("jsmith");
...
    这是如何做到的呢? 这里嘉定所有的对象都是和Java Bean兼容的POJO对象。
    在这种约定的前提下,当给对象设置属性时,Shiro会将所有的脏活、累活都交给Apache Common BeanUtils来干,虽然我们在INI文件内配置的是文本,但是BeanUtils知道如何将一个字符串的值转换为基本类型,并且调用该对象的对应的setter方法来给该POJO设置属性。
    引用类型
    
  如果要设置是引用类型怎么办?你可以用一个美元符号来引用前文中定义的对象,像下面这样,就这么简单。 
 ...
sha256Matcher = org.apache.shiro.authc.credential.Sha256CredentialsMatcher
...
myRealm.credentialsMatcher = $sha256Matcher
...
 
    嵌套属性
    你可以像下面这样引用嵌套属性,给属性赋值,不管有多少个层级,都可以这么用。
...
securityManager.sessionManager.globalSessionTimeout =1800000
...
   BeanUtils会把他翻译成下面这样的代码:
 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需要的字节更少。这显然更合适文本配置。
    
#The'cipherKey' attribute is a byte array.Bydefault, text values
#for all byte array properties are expected to be Base64 encoded:
securityManager.rememberMeManager.cipherKey = kPH+bIxk5D2deZiIxcaaaA==
...
十六进制的文本当然也是可以的,你要记得在文本的前面加上0x。
securityManager.rememberMeManager.cipherKey = 0x3707344A4093822299F31D008
    
    集合属性
    我们可以像设置其他属性一样去设置list、set、map类型的属性,不管是直接的属性还是嵌套属性。对于list和set,我们可以直接使用逗号分隔的值(或者引用)来设置,例如:
sessionListener1 = com.company.my.SessionListenerImplementation
...
sessionListener2 = com.company.my.other.SessionListenerImplementation
...
securityManager.sessionManager.sessionListeners = $sessionListener1, $sessionListener2
    对于map,你可以指定一系列都好分隔的键值对。键值对内部使用 冒号来作为键和值的分隔符,例如:
object1 = com.company.some.Class
object2 = com.company.another.Class
...
anObject = some.class.with.a.Map.property
anObject.mapProperty = key1:$object1, key2:$object2
你还可以直接使用对象来作为key,如下:
    anObject.map = $objectKey1:$objectValue1, $objectKey2:$objectValue2
    
    一些思考
    顺序问题
    在INI文件中配置的顺序决定了他们翻译成Java代码之后的顺序,这块要小心。
 
    重写实例
    后定义的同名对象会覆盖之前定义的对象,如下第二个myRealm会覆盖掉第一个myRealm的定义,所以最终myRealm是com.company.security.DatabaseRealm的对象实例,而前一个定义的realm将永远不会被引用了。(自然也就被垃圾回收了)。
...
myRealm = com.company.security.MyRealm
...
myRealm = com.company.security.DatabaseRealm
...
    默认的SecurityManager
     你可能已经注意到,在上文的配置中,我们并没有创建SecurityManager对象就直接去设置他的嵌套属性了。
myRealm =...
securityManager.sessionManager.globalSessionTimeout =1800000
...
    SecurityManager是特例,系统已经自动为你创建好了一个SecurityManager对象,你就只管用就好了。当然,如果你说我一定要使用字节实现的一个SecurityManager,那也不是不行,直接像下面这么写就可以了,就像 重写实例 章节中说的,这样就会覆盖掉系统自动创建的SecurityManager对象了。
...
securityManager = com.company.security.shiro.MyCustomSecurityManager
...
当然,这种情况很少发生。因为Shiro默认提供的SecurityManager具有很好的定制性,你几乎可以为配置任何属性。所以如果你发现自己要写一个自定义的SecurityManager的时候,不妨先问问自己:这真的有必要吗?
 
    (2) [users]
    [users]Setion允许你定义一组用户账号。在那些只需要很少的用户账号,或者是那些不需要在运行时动态创建用户账号的运行环境下,这个功能很实用。你可以像下面这么写。
[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 属性  即可。
[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进制,则应该像下面这么配置。
[main]
...
#true= hex,false= base64:
sha256Matcher.storedCredentialsHexEncoded =false
    
    (3)[roles]
    这个session用于定义角色与权限的对照关系,同样的,这种配置方式对于那些只有少数几种角色,并且不需要在运行时动态创建角色的应用程序特别实用。
[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)章节来了解更多关于这种权限格式的信息。
    
    小贴士:权限内部的分号
    如果在权限内部要有分号,则在角色定义中必须要用双引号将他们括起来,以避免解析错误,比如printer:5thFloor:print,info,就要写成“printer:5thFloor:print,info”
 
    小贴士:不需要权限的角色定义
    如果你的角色是不需要关联权限的,那么你不需要在[roles]章节中来列出他们,你尽管在[users]章节中使用他们就是了。如果系统中不存在这个角色,则Shiro会自动创建对应的角色。
 
    (4)[urls]
    这个Section的描述将放在 Web 章节。
 
原文地址:http://shiro.apache.org/configuration.html

本系列相关:

Apache Shiro系列四概述 ——Shiro的架构