Apache Shiro Realms 中文说明文档

算法其他 ALLEN ⋅ 于 2020-05-11 19:00:23 ⋅ 52 阅读

这是一个补充文档

一个 Realm 是一个能访问特定应用安全数据的组件,比如 users(用户表),roles (角色表)和 permissions(权限表)。这 Realm 把特定应用的数据转换成 Shiro 能理解的数据格式,所以 Shiro 转而提供一个单一的易于理解的 Subject 可编程 API,而不管数据源有多少个或你的数据是怎样的特定于应用程序。

各Realms 通常与数据源是 1 对 1 的关系,比如关系型数据库,LDAP 目录,文件系统,或者其他类似的资源。同样地,Realm 接口的实现使用特定数据源的 API 来发现授权数据(roles-角色, permissions-权限),比如 JDBC,File IO,Hibernate 或者 JPA,或者 任意其他数据访问 API。

一个 Realm 本质上是 security-specific(特定安全的) DAO

因为这些数据源的大部分通常不仅存储授权数据(比如roles-角色和permissions-权限)也存储验证数据(凭证,比如密码),每个Shiro Realm 都可以执行验证和授权操作。

Realm 配置

如果使用 Shiro’s INI 文件配置,那么你可以像其他任意对象一样在 [main] 片段 定义和引用 Realms ,但是它们是以显示或者隐式形式在 securityManager 上进行配置。

显示赋值

基于目前为止的 INI 配置的知识,这是一个显而易见的配置方式。定义一个或多个 Realms 之后,你可以在 securityManager 对象的集合属性上设置它们。

比如:

fooRealm = com.company.foo.Realm
barRealm = com.company.another.Realm
bazRealm = com.company.baz.Realm

securityManager.realms = $fooRealm, $barRealm, $bazRealm

显示赋值是确定无疑的 - 你不仅可以控制它们用于认证和授权的顺序而且还可以准确控制那个 realms 被使用。Realm 顺序的影响在认证一章中的 认证序列 一节详细描述。

隐式赋值

如果你改变了 realms 定义的顺序,那么隐式赋值会导致意想不到的行为。推荐是避免使用这种方式而是使用显示赋值,其具有确定性的行为。隐式赋值很可能在 Shiro 将来的发布版本中被移除。
如果因为某些原因你不想用显示赋值配置 securityManager.realms 属性,那么你可以让 Shiro 检测所有可配置的 realms 并且把它们直接赋值给 securityManager。

使用隐式方法,realms会根据它们定义的顺序被赋值给 securityManager 实例。

也就是,对于下面的 shiro.ini 例子:

blahRealm = com.company.blah.Realm
fooRealm = com.company.foo.Realm
barRealm = com.company.another.Realm

# no securityManager.realms assignment here

本质上,下面的代码行有着一样的效果:

securityManager.realms = $blahRealm, $fooRealm, $barRealm

但是,意识到使用隐式赋值,realms 定义的顺序会直接影响到验证和授权的方式。如果你改变了它们的定义顺序,则将更改主身份验证器 身份验证序列 函数。

由于这个原因,并且为了确保确定性行为,我们推荐使用显示赋值而不是隐式赋值。## Realm 身份验证
一旦你理解了 Shiro’s 主身份验证流程, 在身份验证尝试期间,准确地知道身份验证器与 Realm 交互时发生了什么是很重要的。

支持身份验证Tokens

正如在验证流程提到的,就在 Realm 执行身份验证尝试之前,它的 supports 方法会被调用。如果返回值是 true,那么之后只有 getAuthenticationInfo(token) 方法会调用。

通常,realm 会检查所提交的 token的类型(接口类型或者类类型)来判断是否可处理。 比如,处理生物数据的 Realm 可能根本不理解 UsernamePasswordTokens,这种情况,supports 方法会返回 false。

处理 支持的身份验证Tokens

如果 Realm 支持所提交的 AuthenticationToken,那么 Authenticator(身份验证器)会调用 Realm 的 getAuthenticationInfo(token) 方法。这实际上表示使用 Realm 的后端数据源进行身份验证尝试。该流程按顺序描述如下:

  • 为验证 principal (账号认证信息)检查 token
  • 基于 principal, 在数据源寻找相应的账号数据。
  • 确认提供的 token 凭证匹配存储中的 token 数据。
  • 如果凭证匹配,那么返回一个 Shiro 能理解的格式并且包含账号数据的 AuthenticationInfo 实例。
  • 如果凭证不匹配,那么会抛出 AuthenticationException 异常。

这是所有 Realm getAuthenticationInfo 实现的最高级别工作流。在这个方法里,Realms 可以做任意它们想做的事情,比如在审计日志中记录身份验证尝试,更新数据记录,或者对数据存储的身份验证尝试有意义的其他事情。

唯一需要的事是,如果凭证匹配给定的 principal(s),那么返回一个非空AuthenticationInfo 实例,该实例表示来自该数据源的 Subject 帐户信息。

直接实现 “Realm” 接口可能很费时,而且容易出错。大多数人选择将AuthorizingRealm 抽象类子类化(使用继承 AuthorizingRealm 抽象类),而不是从零开始。该类实现了通用的身份验证和授权工作流,以节省您的时间和精力。

凭证匹配

在上述 realm 验证工作流中,一个 Realm 必须验证 Subject 对象所提交的凭证(比如密码)是否与存储在数据存储中的凭证匹配。如果它们匹配,那么身份验证成功,并且此时系统已经验证了 end-user 的凭证。

对提交的凭证与 Realm 后端数据存储的凭证进行匹配是 Realm 的职责,而不是 ‘Authenticator’ 的职责。每个 'Realm' 都熟悉凭证格式和存储,可以执行详细的凭证匹配,而 'Authenticator' 是通用的工作流组件。

凭证匹配过程在所有应用几乎是相同的,并且通常只有数据比较而不同。为了确保这个过程是可插拔的,并在必要时可定制的,AuthenticatingRealm 对象以及它的子类支持 CredentialsMatcher 的概念来执行凭证比较。

发现账号数据后,AuthenticatingRealm和所提交的 AuthenticationToken 呈现给 CredentialsMatcher 来判断所提交的与数据存储内的凭证是否匹配。

Shiro 有可以让你开始使用的 CredentialsMatcher 实现, 比如 SimpleCredentialsMatcherHashedCredentialsMatcher 实现,但是如果你想为自定义匹配逻辑配置自定义实现,那么你应该直接这样做:

Realm myRealm = new com.company.shiro.realm.MyRealm();
CredentialsMatcher customMatcher = new com.company.shiro.realm.CustomCredentialsMatcher();
myRealm.setCredentialsMatcher(customMatcher);

或者,如果你使用 Shiro 的 INI 配置:

[main]
...
customMatcher = com.company.shiro.realm.CustomCredentialsMatcher
myRealm = com.company.shiro.realm.MyRealm
myRealm.credentialsMatcher = $customMatcher
...

简单的相等性检查

Shiro 所有开箱即用(out-of-the-box)的 Realm 默认实现是使用 SimpleCredentialsMatcher。 SimpleCredentialsMatcher 对存储的帐户凭据和 AuthenticationToken 中提交的凭证执行简单的直接相等性检查。

例如,如果提交了一个UsernamePasswordToken, SimpleCredentialsMatcher 将验证提交的密码是否与数据库中存储的密码完全相同。

SimpleCredentialsMatcher 不仅仅对字符串执行直接相等比较。它可以处理最常见的字节源,如字符串、字符数组、字节数组、文件和 InputStreams 。更多信息请参见 JavaDoc 说明文档。

Hash 化的凭证

存储最终用户凭证(例如密码)的一种更安全的方法是,在将凭证存储到数据存储之前先对其进行单向哈希,而不是以原始形式存储凭证并执行原始的 / 直接的比较。
这确保了最终用户的凭证永远不会以原始形式存储,也没有人能够知道原始(original)/原本(raw)值。这是一种比纯文本或原始格式比较更安全的机制,所有安全意识强的应用程序都应该支持这种方法,而不是非 hash 存储。

为了支持这些首选的加密哈希策略,Shiro 提供了在 realms 中可配置的HashedCredentialsMatcher 实现,而不是前面提到的 SimpleCredentialsMatcher。
哈希凭证以及盐析化和多重哈希迭代的好处超出了这个 Realm 文档的知识范围,但是一定要阅读HashedCredentialsMatcher JavaDoc ,其中详细介绍了这些原则。

哈希和相应的匹配器
所以你该怎样配置一个使用 Shrio 的应用程序使得哈希匹配简单化?

Shiro 提供了多个 HashedCredentialsMatcher 子类的实现。你必须在你的 realm 中配置具体的 HashedCredentialsMatcher 实现以符合你使用的使你的用户凭证哈希化的哈希算法。

比如,我们假定你的应用程序使用 用户名/密码 对进行验证。并且由于上述描述的哈希凭证的优点,我们假定当你创建一个用户账号时,你想使用 SHA-256 算法来单向哈希用户的密码。你应该哈希由用户输入的明文密码并且对其进行保存:

import org.apache.shiro.crypto.hash.Sha256Hash;
import org.apache.shiro.crypto.RandomNumberGenerator;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
...

//We'll use a Random Number Generator to generate salts.  This 
//is much more secure than using a username as a salt or not 
//having a salt at all.  Shiro makes this easy. 
//
//Note that a normal app would reference an attribute rather 
//than create a new RNG every time: 
RandomNumberGenerator rng = new SecureRandomNumberGenerator();
Object salt = rng.nextBytes();

//Now hash the plain-text password with the random salt and multiple 
//iterations and then Base64-encode the value (requires less space than Hex): 
String hashedPasswordBase64 = new Sha256Hash(plainTextPassword, salt, 1024).toBase64();

User user = new User(username, hashedPasswordBase64);
//save the salt with the new account.  The HashedCredentialsMatcher 
//will need it later when handling login attempts: 
user.setPasswordSalt(salt);
userDAO.create(user);

既然是使用 SHA-256 对用户密码进行哈希化,那么你需要告诉 Shiro 得用合适的 HashedCredentialsMatcher 来匹配你的哈希方式。在这个例子,我们创建了一个随机的盐值(salt)并为强安全性(可翻阅 HashedCredentialsMatcher JavaDoc 来获得原因)会执行 1024 次 hash 迭代。这里使用 Shiro INI 的配置文件来实现:

[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 接口确保了你在创建用户账号时所使用的salt 值(比如 上面调用的user.setPasswordSalt(salt);)可以被 HashedCredentialsMatcher 所使用。
HashedCredentialsMatcher 需要 salt 值是为了在数据存储中的 token 与提交的 AuthenticationToken 进行匹配时以执行相同的 hashing 技术,所以如果你给你的用户密码使用 salting 技术(并且你应该用!!!)时,请确保你的 Realm 实现能体现出返回 SaltedAuthenticationInfo 实例。

取消验证

如果由于某个原因,对于某个数据源,你不想使用 Realm 来执行验证操作(可能你只是想让 Realm 来执行授权操作),那么你可以通过在 Realm 相应方法上直接返回 false 就可以完全取消 Realm 对验证功能的支持。之后在验证请求过程中你的 realm 就不会去处理验证请求。

当然,如果你想验证 Subjects,那么至少有一个已配置的 Realm 需要能支持 AuthenticationTokens的处理。

Realm 授权

SecurityManager 把 Permission 或者 Role 检查转交给 Authorizer,默认是对 ModularRealmAuthorizer 处理。

基于角色授权
当其中一个重载方法在 Subject 上调用 hasRoles 或 checkRoles 方法时:

  • Subject 把判断给定的 Role 是否已经被赋予的任务转交给 SecurityManager;
  • SecurityManager 然后转交给 Authorizer;
  • Authorizer 然后一个个的查询所有的 Authorizing Realms,直到发现给定的 role 已经赋予给这个 subject。 如果没有一个 Realm 赋予该 Subject 给定的 Role,那么通过返回 false 来拒绝访问;
  • 调用 Realm AuthorizationInfo的 getRoles() 来获得所有赋予给 Subject 的所有角色;
  • 如果给定的 Role 在 AuthorizationInfo.getRoles 方法返回的 role 列表中,则赋予访问权限;

基于许可授权
当其中一个重载方法在 Subject 上调用 isPermitted() 或 checkPermission() 方法时:

  • Subject 把赋予或拒绝许可的任务转交给 SecurityManager;
  • SecurityManager 转交给 Authorizer;
  • Authorizer 然后一个个地查询所有的 Authorizer Realms,直到 Permission 被赋予。如果 Permission 没有被任何 Authorizing Realm 赋予,那么该 Subject 被拒绝访问 Permission。
  • Authorizing Realm 接着做如下事情来检查一个 Subject 是否被许可:
    a. 首先,通过在 AuthorizationInfo 上调用 getObjectPermissions() 和 getStringPermissions() 来识别赋予给 Subject 的所有 Permissions,并且集合该结果。
    b. 如果一个 RolePermissionResolver 被注册,那么通过调用 RolePermissionResolver.resolvePermissionsInRole()来获得 Subject 的基于角色的所有 Permissions。
    c. 对于步骤 a 和步骤 b获得的 Permissions,调用 implies() 方法来检查这些许可是否隐含所要检查的许可。 详情可参考 WildcardPermission

追求梦想,做最好的自己

点赞
回复数量: 0
    暂无评论~~
    • 请注意单词拼写,以及中英文排版,参考此页
    • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`, 更多语法请见这里 Markdown 语法
    • 支持表情,使用方法请见 Emoji 自动补全来咯,可用的 Emoji 请见 :metal: :point_right: Emoji 列表 :star: :sparkles:
    • 上传图片, 支持拖拽和剪切板黏贴上传, 格式限制 - jpg, png, gif
    • 发布框支持本地存储功能,会在内容变更时保存,「提交」按钮点击时清空
      请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!
    Ctrl+Enter