田雄兵 2018-04-27
Apache Shiro安全框架配置详解
---By Solo Cui
Shiro可以在任何环境下工作,从简单的命令行应用程序到最大的企业集群应用程序。 由于这种环境的多样性,有许多适合配置的配置机制。 本节仅介绍Shiro内核支持的配置机制。
另外,Shiro的SecurityManager实现和所有支持组件都与JavaBean兼容。 这使得Shiro可几乎任何配置格式进行配置,例如常规Java、XML(Spring,JBoss,Guice等)、YAML、JSON、Groovy Builder标记等等。
下面主要介绍如下两种配置模式,即编程式配置和INI文件式配置。
创建SecurityManager并将使其对应用程序可用,实现此目的的绝对最简单的方法是创建一个org.apache.shiro.mgt.DefaultSecurityManager并以代码形式将其连接起来。 例如:
Realm realm = //实例化或或获取一个Realm实例,后面将讨论Realms. SecurityManager securityManager = new DefaultSecurityManager(realm); //通过静态内存构建针对整个应用程序的可用SecurityManager实例: SecurityUtils.setSecurityManager(securityManager); |
令人惊讶的是,仅需3行代码,您就可以拥有适用于多种应用的功能齐全的Shiro环境。 那有多容易a!?
正如架构章节中所讨论的,Shiro的SecurityManager实现本质上是特定安全组件嵌套的模块化对象图。 因为它们也是JavaBeans兼容的,所以可以调用任何嵌套的组件getter和setter方法来配置SecurityManager及其内部对象图。
例如,如果您想将SecurityManager实例配置为使用自定义的SessionDAO来定制会话管理,则可以使用嵌套的会话管理器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格式来构建SecurityManager对象图及其支持组件。 INI文件易于阅读、易于配置、设置简单,并且适合大多数应用。
以下是如何基于INI配置来构建安全管理器SecurityManager的两个示例。
1-从INI资源文件创建SecurityManager
我们可以从INI资源路径创建SecurityManager实例。 当分别以file:,classpath 或url:为前缀时,可以从文件系统,类路径或URL获取资源。 本示例使用Factory从类路径的根目录获取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); |
2-从INI实例构建SecurityManager
如果需要,INI配置也可以通过org.apache.shiro.config.Ini类以编程方式构建。 Ini类的功能类似于JDK java.util.Properties类,但另外还支持按部分名称进行分段。例如:
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配置。
INI基本上是由唯一命名的部分组成的键/值对组成的文本配置。 键在每个部分都是唯一的,而不是整个配置(与JDK属性不同)。 可是每个部分可以像单个属性定义一样被查看。
注释行可以井号(# - 又名'hash','pound'或'number'符号)或分号(';')开始
这是Shiro所能理解的一个例子的部分:
[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实例及其任何依赖关系(如Realm)的位置。
与SecurityManager或其任何依赖关系类似,配置对象实例听起来像是一个难以处理的INI文件,文件中我们只能使用名称/值对。 但是通过对象图的一些约定和理解,你会发现你可以做很多事情。 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]部分片段:
此行实例化一个类型为com.company.shiro.realm.MyRealm的新对象实例,并使该对象在myRealm名称下可用,以供进一步引用和配置。
如果实例化的对象实现了org.apache.shiro.util.Nameable接口,那么Nameable.setName方法将在具有名值的对象(本例中为myRealm)上被调用。
设置对象属性
1)基本值
简单的基本属性可以通过使用等号来赋值:
这些配置行转换为方法调用如下所示:
这怎么可能? 因为它假定所有对象都是Java Bean兼容的POJO。
在背后,Shiro默认使用Apache Commons BeanUtils在设置这些属性时完成所有繁重的工作。 因此,虽然INI值是文本,但BeanUtils知道如何将字符串值转换为适当的基本类型,然后调用相应的JavaBeans setter方法。
2)引用值
如果你需要设置的值不是基本的,而是另一个对象呢? 那么,您可以使用美元符号($)来引用先前定义的实例。 例如:
这只是简单地定位名称sha256Matcher定义的对象,然后使用BeanUtils在myRealm实例上设置该对象(通过调用myRealm.setCredentialsMatcher(sha256Matcher)方法)。
3)嵌套属性
在INI行等号的左侧使用点符号,可以遍历一个对象图以获得您想要设置的最终对象/属性。 例如,下面这个配置行:
将(通过BeanUtils)转换为以下逻辑:
图遍历可以尽可能深:object.property1.property2 .... propertyN.value = blah
提示:BeanUtils属性支持 任何由BeanUtils.setProperty方法支持的属性赋值操作都可以在Shiro的[mai]n节中使用,包括set/list/map元素赋值。 有关更多信息,请参阅Apache Commons BeanUtils(http://commons.apache.org/proper/commons-beanutils/)网站和文档。 |
4)字节数组值
由于原始字节数组不能以文本格式本地化指定,因此我们必须使用字节数组的文本编码。 这些值可以指定为Base64编码的字符串(默认值),也可以指定为十六进制编码的字符串。 默认是Base64,因为Base64编码需要较少的实际文本来表示值 - 它有一个更大的编码字母表,这意味着您的令牌更短(这对于文本配置更好一点)。
但是,如果您更愿意使用十六进制编码,则必须在字符串标记前缀0x('零''x'):
5)集合属性
列表、集合和映射可以像任何其他属性一样设置——直接或作为嵌套属性。 对于集合和列表,只需指定逗号分隔的一组值或对象引用即可。如下面的示例:
对于映射,您可以指定以逗号分隔的键值对列表,其中每个键值对由冒号":"分隔。
在上例中,由$object1引用的对象将位于String键key1下的映射中,即map.get("key1")返回object1。 您也可以使用其他对象作为键,如下:
6)注意事项
6-1)顺序问题
上述INI格式和约定非常方便且易于理解,但并不像其他文本/基于XML的配置机制那么强大。 在使用上述机制时要理解的最重要的事情是顺序问题(Order Matters)!
小心:每个对象实例化和每个值赋值是按它们在[main]节点中出现的顺序执行。 这些行最终转化为JavaBeans的getter/setter方法调用,所以这些方法以相同的顺序调用!
在编写配置时请记住这一点。
7)覆盖实例
任何对象都可以被稍后在配置中定义的新实例覆盖。 例如,第二个myRealm定义会覆盖第一个:
这将导致myRealm成为com.company.security.DatabaseRealm实例,并且以前的实例将永远不会被使用(且将被垃圾收集)
8)默认安全管理器(SecurityManager)
您可能在上面的完整示例中已经注意到SecurityManager实例的类未定义,并且我们直接跳转到设置嵌套属性:
这是因为securityManager实例是一个特殊的实例 - 它已经为您实例化并准备就绪,因此您不需要知道要实例化的特定SecurityManager实现类。
当然,如果你真的想指定你自己的实现,你可以按照上面的"重写实例"一节中的规定来定义你的实现:
当然,这很少需要——Shiro的SecurityManager实现是非常可定制的,通常可以配置任何必要的东西。 但您可能需要问自己(或用户列表)是否您确实需要这样做?
2-[users]部分
[users]部分允许您定义一组静态的用户帐户。 这在用户帐户数量非常少的环境或用户帐户不需要在运行时动态创建的环境中非常有用。 这是一个例子:
[users] admin = secret lonestarr = vespa, goodguy, schwartz darkhelmet = ludicrousspeed, badguy, schwartz |
提示:自动iniRealm
只要定义非空的users或roles部分,就会自动触发org.apache.shiro.realm.text.IniRealm实例的创建,并使其在名为iniRealm的[main]部分中可用。 您可以像上面所描述的那样将其配置为任何其他对象。
1)行格式
[users]部分中的每一行必须符合以下格式:
username
= password
, roleName1, roleName2, …, roleNameN
•等号左边的值是用户名
•等号右边的第一个值是用户的密码。 密码是必需的。
•密码后的任何逗号分隔值是分配给该用户的角色的名称。 角色名称是可选的。
2)加密密码
如果您不希望[users]部分的密码为纯文本格式,则可以使用您喜欢的哈希算法(MD5,Sha1,Sha256等)对其进行加密,无论您是否喜欢,并将结果字符串用作密码值。 默认情况下,密码字符串预计为十六进制编码,但可以配置为Base64编码(参见下文)。
提示:便捷的密码安全处理
为了节省时间并使用最佳实践,您可能需要使用Shiro的命令行哈希器,它会散列密码以及任何其他类型的资源。 加密INI[users]部分密码特别方便。
一旦你指定了散列文本密码值,你必须告诉Shiro它们已被加密。 您可以通过在main部分配置隐式创建的iniRealm来使用与您指定的哈希算法相对应的适当的CredentialsMatcher实现:
您可以像任何其他对象一样配置CredentialsMatcher上的任何属性以反映您的散列策略,例如,指定是否使用salting或执行多少个散列迭代。 请参阅org.apache.shiro.authc.credential.HashedCredentialsMatcher JavaDoc(http://shiro.apache.org/static/current/apidocs/org/apache/shiro/authc/credential/HashedCredentialsMatcher.html)以更好地理解哈希策略,以及它们是否对您有用。
例如,如果用户的密码字符串是Base64编码,而不是默认的十六进制,则可以指定:
3-[roles]部分
[roles]部分允许您将权限与用户部分中定义的角色相关联。 同样,这在具有少量角色的环境中或角色不需要在运行时动态创建的情况下很有用。 这是一个例子:
[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 |
1)行格式
[roles]部分中,每一行必须必须以下列格式定义角色权限(s)键/值映射:
rolename
= permissionDefinition1, permissionDefinition2, …, permissionDefinitionN
其中permissionDefinition是一个任意字符串,但从方便使用和灵活性上考虑,大多数人都希望使用符合org.apache.shiro.authz.permission.WildcardPermission格式的字符串。 有关权限的更多信息以及如何从中受益,请参阅权限文档。
提示:内部逗号
请注意,如果单个permissionDefinition需要在内部逗号分隔(例如,printer:5thFloor:print,info),则需要用双引号(")括住该定义以避免解析错误:"printer:5thFloor:print,info "。
如果您的角色不需要权限关联,则不需要在[roles]部分列出它们。 如果它还不存在,只要在[users]部分中定义角色名称就足以创建角色。
4-[urls]部分
本章节及其选项在Web章节中有描述。这里重复描述,具体内容如下:
[urls]部分中每行的格式如下:
_URL_Ant_Path_Expression_ = _Path_Specific_Filter_Chain_
示例如下:
接下来我们将阐述这些行的意思。
等号(=)左侧的标记是相对于Web应用程序的上下文根的Ant式路径表达式。
例如,假设您有以下[urls]行:
/account/** = ssl, authc
此行描述的是:"对我的应用程序/account路径或其任何子路径(/account/foo,/account/bar/baz等)的任何请求都会触发'ssl,authc'过滤器链"。 我们下面将描述过滤器链。
请注意,所有路径表达式都与您的应用程序的根上下文相关。 这意味着如果您有一天将应用程序部署到www.somehost.com/myapp,然后又将其部署到www.anotherhost.com(没有'myapp'子路径),则模式匹配仍然可以工作。 所有路径都与HttpServletRequest.getContextPath()值有关。
小心:顺序问题
URL路径表达式按照它们传入请求的定义顺序和“FIRST MATCH WINS”(首先匹配获胜)进行鉴定。 例如,让我们假设有以下定义链:
如果传入的请求打算到达/account/signup/index.html(可供所有'匿名'用户访问),它将永远不会被处理! 原因是/ account / **模式首先匹配传入请求,并将所有剩余定义"短路"。
永远记得根据FIRST MATCH WINS策略定义您的过滤器链!
1)过滤链定义
等号(=)右侧的标记是用逗号分隔的过滤器列表,用于执行与该路径匹配的请求。 它必须符合以下格式:
filter1[optional_config1], filter2[optional_config2], ..., filterN[optional_configN]
其中:
• filterN是[main]部分中定义的过滤器bean的名称,并且
• [optional_configN] 是一个可选的括号内的字符串,对那个特定路径的特定过滤器有意义(每个过滤器,特定于路径的配置!)。 如果过滤器不需要该URL路径的特定配置,则可以放弃括号,因此filterN[] 只会变成filterN。
并且因为过滤令牌定义了链(又名列表),请记住该顺序很重要! 按照您希望请求流经的顺序定义您的逗号分隔链列表。
最后,如果不满足其必要条件(例如,执行重定向,用HTTP错误代码响应,直接渲染等),每个过滤器都可以自由处理响应。 否则,按照预期,它会允许请求经由过滤链,到最终的目标视图。
提示:
能够对特定路径的配置作出反应,即过滤器令牌的[optional_configN]部分,是Shiro过滤器可用的独特功能。
如果你想创建自己的javax.servlet.Filter实现,也可以这样做,确保你的过滤器为org.apache.shiro.web.filter.PathMatchingFilter的子类。
2)可用过滤器
可用于过滤器链定义的"过滤器"池在[main]部分定义。 在main部分分配给他们的名称是过滤器链定义中使用的名称。 例如:
3)默认过滤器
运行Web应用程序时,Shiro将创建一些有用的默认过滤器实例,并自动在main部分使其可用。 您可以像在任何其他bean中一样配置它们,并在链定义中引用它们。 例如:
[main] ... #注意,我们没有定义FormAuthenticationFilter("authc")类,但它却已经实例化并可用: authc.loginUrl = /login.jsp ... [urls] ... # 确保最终用户通过身份验证. 否则重新定向到上面的 'authc.loginUrl' 上, # 成功认证后, 重新定向到他们尝试查看的 : /account/** = authc ... |
可用的默认Filter实例自动由枚举类DefaultFilter完成定义,枚举的name字段是可用于配置的名称。 他们是:
4)启用禁止过滤器
与任何过滤器链定义机制(web.xml,Shiro的INI等)一样,只需将过滤器包含在过滤器链定义中即可启用该过滤器,并通过将其从链定义中删除来禁用过滤器。
但Shiro 1.2中新增的一项新功能是可以启用或禁用滤波器,而无需从滤波器链中删除滤波器。 如果启用(默认设置),则会按预期过滤请求。 如果禁用,则过滤器将允许请求立即通过FilterChain中的下一个元素。 通常可以基于配置属性触发过滤器的启用状态,或者甚至可以基于每个请求触发它。
这是一个强大的概念,因为根据某些要求启用或禁用过滤器通常比更改静态过滤器链定义更方便,而静态过滤器链定义是永久性且不灵活的。
Shiro通过它的OncePerRequestFilter抽象父类完成此操作。 所有Shiro的开箱即用滤波器实现都是这个子类的一个子类,因此可以在不从滤波器链中删除它们的情况下启用或禁用它们。 如果您也需要此功能,您可以将此类继承为您自己的过滤器实现*。
*SHIRO-224(https://issues.apache.org/jira/browse/SHIRO-224)将有希望为任何过滤器启用此功能,而不仅仅是那些子类OncePerRequestFilter。 如果这对你很重要,请为此问题投票。
5)通用启用/禁用
OncePerRequestFilter(及其所有子类)支持跨所有请求启用/禁用过滤器,与基于每个请求的一样。
通常为所有请求启用或禁用过滤器是通过将其enabled属性设置为true或false来完成的。 默认设置为true,因为大多数滤波器在链中配置时是固化需要执行的。
例如,在shiro.ini中:
这个例子表明,很多URL路径可能都需要一个请求必须通过SSL连接来保护。 在开发过程中设置SSL可能令人沮丧且费时。 在开发过程中,您可以禁用SSL过滤器。 部署到生产环境时,您可以使用一个配置属性启用它 - 这比手动更改所有URL路径或维护两个Shiro配置要容易得多。
6)特殊请求的启用和禁用
OncePerRequestFilter根据isEnabled(request,response)方法实际确定是启用还是禁用过滤器。
此方法默认返回enabled属性的值,该属性用于通常启用/禁用上述所有请求。 如果您想根据请求特定条件启用或禁用过滤器,则可以覆盖OncePerRequestFilter.isEnabled(request,response)方法以执行更具体的检查。
7)特殊路径的启用和禁用
Shiro的PathMatchingFilter(OncePerRequestFilter的一个子类)可以根据被过滤的特定路径对配置做出反应,这意味着除了传入的请求和响应之外,还可以根据路径和路径特定的配置启用或禁用过滤器。
如果您需要能够响应匹配路径和特定于路径的配置以确定过滤器是启用还是禁用,而不是重写OncePerRequestFilter.isEnabled(request,response)方法,则可以覆盖PathMatchingFilter.isEnabled(request,response ,path,pathConfig)方法。