一个Realm可以理解为能够访问特定于应用程序的安全数据(如用户,角色和权限)的组件。不管应用程序存在多少数据源以及任何类型的数据,Shiro都能通过Realm将其转换成易于理解的的程序化API格式。
Realm通常跟数据源(例如关系数据库,LDAP目录,文件系统或其他类似资源)具有1对1的关联关系。因此,Realm接口的实现使用特定于数据源的API来发现授权数据(角色,权限等),例如JDBC,文件IO,Hibernate或JPA或任何其他数据访问的API。
因为通常情况下,大部分数据源既有认证数据(类似于密码)又有授权数据(类似于角色和权限),所以Shiro的Realm可以对认证和授权同样支持。
Realm配置
如果使用Shiro的INI配置,您可以将Realms
像[main]
部分中的任何其他对象一样去定义和引用,它们可以通过securityManager使用以下两种方式之一进行配置:显式或隐式。
明确的分配
基于之前对INI配置的了解,这是一种申明式的配置方法。定义一个或多个Realms后,将它们设置为securityManager对象的集合属性。
例如:
fooRealm = com.company.foo.Realm
barRealm = com.company.another.Realm
bazRealm = com.company.baz.Realm
securityManager.realms = $fooRealm, $barRealm, $bazRealm
显式赋值是有确定性的 - 您可以准确地控制使用哪些Realm以及用于身份验证和授权的顺序。
隐含分配
如果基于某种原因您不想显式配置securityManager.realms
属性,则可以允许Shiro检测所有已配置的Realm
并将其直接分配给securityManager
。
使用此方法,域将按照定义的顺序分配给securityManager
实例。
也就是说,对于以下shiro.ini示例:
blahRealm = com.company.blah.Realm
fooRealm = com.company.foo.Realm
barRealm = com.company.another.Realm
它基本上和下面的展示有一样的效果
securityManager.realms = $blahRealm, $fooRealm, $barRealm
如果你改变了Realms的定义顺序,隐式配置可能会造成一些未预期的影响。所以正常情况下建议用户使用显示配置。
Realm认证
如果您需要掌握Shiro的认证流程,那么了解在认证过程中Authenticator
与Realm
是如何交互则显得格外重要。
支持AuthenticationTokens
在认证流程中我们已经提及到,在认证步骤之前我们就会调用到Realm
的supports
方法,只有当它返回true
,getAuthenticationInfo(token)
方法才会被执行。
通常Realm
会检查提交上来的认证token是否是其支持的类型或class,例如,处理生物识别数据的Realm
可能根本不理解UsernamePasswordTokens
,在这种情况下它将从supports方法返回false。
处理支持的AuthenticationTokens
如果Realm
支持提交上来的AuthenticationToken
,Authenticator
会调用Realm的getAuthenticationInfo(token)
方法。这实际上代表了对Realm数据源的身份验证尝试。该方法依次为:
- 检查
token
识别主体(帐户识别信息) - 基于识别到的
principal
,查找数据源中的相应帐户数据 - 确保提供的令牌
credentials
与存储在数据存储中的令牌相匹配 - 如果凭据匹配,则返回
AuthenticationInfo
实例,该实例会以Shiro理解的格式来封装帐户数据 - 如果凭据不匹配,则抛出
AuthenticationException
这是所有Realm对实现getAuthenticationInfo的最高级别工作流程。Realm可以在此方法中随意执行任何操作,例如在审计日志中记录认证操作,更新数据记录或对该数据存储的身份验证尝试任何有意义的其他内容。
唯一需要注意的是,如果凭证与给定主体匹配,AuthenticationInfo
方法要返回来自该数据源的主体信息的一个非空实例。
凭证匹配
在上述Realm
身份验证工作流程中,Realm
必须验证Subject
提交的凭据(例如密码)与存储在数据存储中的凭据匹配。如果匹配,则认为认证成功,并且系统已验证最终用户的身份。
凭证匹配过程在所有应用程序中几乎相同,并且通常仅仅不同于需要比较的数据。为确保此过程可插拔并在必要时可自定义,AuthenticatingRealm
及其子类支持CredentialsMatcher
的概念来执行凭据比较。
在发现用户数据之后,将其和提交上来的AuthenticationToken
信息一同交给CredentialsMatcher
来判断提交的内容是否与存储在数据库中的内容相匹配。
Shiro中有一些CredentialsMatcher
实现可以让您开箱即用,例如SimpleCredentialsMatcher
和HashedCredentialsMatcher
实现,但如果您想自定义一些匹配逻辑的实现,您可以直接执行:
Realm myRealm = new com.company.shiro.realm.MyRealm();
CredentialsMatcher customMatcher = new com.company.shiro.realm.CustomCredentialsMatcher();
myRealm.setCredentialsMatcher(customMatcher);
或者在INI配置中添加以下内容:
[main]
...
customMatcher = com.company.shiro.realm.CustomCredentialsMatcher
myRealm = com.company.shiro.realm.MyRealm
myRealm.credentialsMatcher = $customMatcher
...
简单的相等性检查
Shiro所有开箱即用的Realm
都是基于SimpleCredentialsMatcher
实现的。SimpleCredentialsMatcher
会简单的基于提交上来的AuthenticationToken的字面意义与存储的认证信息直接匹配。
例如,如果一个UsernamePasswordToken被提交,SimpleCredentialsMatcher
会验证其密码是否和数据库中的密码是否一样。
SimpleCredentialsMatcher
同时支持对字节数组,字符串,字符,文件以及输入流等类型的数据来比较判断相等性。
哈希证书
相比于直接将用户的密码存储到数据库中,一个更安全的方式就是存储用户密码的唯一哈希密文。
确定用户密码不会被直接存到数据库中且没有人知道其原始值与直接将密码以文本格式存储相比更安全,所有考虑到安全相关的应用都应该将其优先于非散列存储。
为了支持这些首选的加密散列策略,Shiro提供了HashedCredentialsMatcher
来替代SimpleCredentialsMatcher
,其支持加盐和多重散列迭代等策略。
哈希和相应的匹配者
那么如何轻松完成配置一个支持Shiro的应用程序呢?
Shiro提供了多个HashedCredentialsMatcher
子类实现。您可以在Realm
上配置特定实现以用于匹配散列用户凭据的散列算法。
例如,假设您的应用程序使用用户名/密码对进行身份验证。由于上面描述的散列凭证的好处,假设您希望在创建用户帐户时使用SHA-256
算法单向散列用户密码。您将用户输入的纯文本密码散列并保存该值:
import org.apache.shiro.crypto.hash.Sha256Hash;
import org.apache.shiro.crypto.RandomNumberGenerator;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
...
//我们会使用一个随机数算法来生成盐 。
//这比直接使用用户名作盐更安全。
//通常app会直接使用RNG的引用,而不是自己定义。
RandomNumberGenerator rng = new SecureRandomNumberGenerator();
Object salt = rng.nextBytes();
//现在用户密码已经加盐且实现了多重迭代的散列,最后使用Base64算法生成字符串
String hashedPasswordBase64 = new Sha256Hash(plainTextPassword, salt, 1024).toBase64();
User user = new User(username, hashedPasswordBase64);
//将获取到的用户的新凭证存储到数据库中,以备登录使用。
user.setPasswordSalt(salt);
userDAO.create(user);
因为您使用了SHA-256
算法来散列您的密码,所以您需要告诉Shiro应该使用HashedCredentialsMatcher
方法来匹配您的认证过程。
例如:
[main]
...
credentialsMatcher = org.apache.shiro.authc.credential.Sha256CredentialsMatcher
# base64 encoding, not hex in this example:
credentialsMatcher.storedCredentialsHexEncoded = false
credentialsMatcher.hashIterations = 1024
# This next property is only needed in Shiro 1.0\. Remove it in 1.1 and later:
credentialsMatcher.hashSalted = true
...
myRealm = com.company.....
myRealm.credentialsMatcher = $credentialsMatcher
...
SaltedAuthenticationInfo
要确保此工作正常的最后一件事是您的Realm自定义实现必须返回SaltedAuthenticationInfo
实例而不是正常AuthenticationInfo
实例。所述SaltedAuthenticationInfo
接口可确保您在创建用户帐户中使用的盐(例如,user.setPasswordSalt(salt);
)可以被HashedCredentialsMatcher
引用。
禁用身份验证
如果由于某种原因,您不希望Realm
对数据源执行身份验证(可能因为您只希望Realm
执行授权),您可以通过始终从Realm
的supports
方法返回false
来完全禁用Realm
对身份验证的支持。然后身份验证期间将永远不会咨询您的Realm
。
当然,Realm如果要对Subjects进行身份验证,至少有一个配置需要能够支持AuthenticationTokens
。
Realm授权
SecurityManager将检查Permission或Role的任务委托给Authorizer的,其默认实现为ModularRealmAuthorizer。
基于角色的授权
Subjec
t委托SecurityManager
来确认是否被分配了指定的角色信息- 然后
SecurityManager
委托给Authorizer
- Authorizer逐个引用所有的授权Realm,直到它找到分配给
Subject
的给定角色。如果没有任何Realm
授予Subject
给定角色,则拒绝访问并返回false - 授权的Realm的
AuthorizationInfo.getRoles()
方法会获取分配给Subject
的所有角色 - 如果在
AuthorizationInfo.getRoles
方法返回的角色列表中找到给定角色,则授予访问权限。
基于权限的授权
当在Subject上调用isPermitted()或checkPermission()其中一个重载方法时:
Subject
委派SecurityManager
判断授予或拒绝任务的权限- 然后
SecurityManager
委托给Authorizer
- 然后,
Authorizer
逐个引用所有授权Realm
,直到权限被授予,如果任何授权Realm
均未授予权限,则拒绝 - 授权
Realm
执行以下操作以检查Subject
是否被允许:
a. 首先,它通过在AuthorizationInfo
上调用getObjectPermissions()
和getStringPermissions
方法并聚合结果来直接识别分配给Subject的所有权限。
b. 如果注册了RolePermissionResolver
,则会调用RolePermissionResolver.resolvePermissionsInRole()
方法来获取分配给Subject的所有角色的权限。
c. 对于来自a和b的聚合权限。调用implies()
方法来检查当前Subject
是否包含所需权限。