shiro 系列

shiro-00-overview

Shiro-01-shiro 是什么?

Shiro-02-shiro 的架构设计详解

Shiro-03-5 分钟入门 shiro 安全框架实战笔记

Shiro-04-Authentication 身份验证

Shiro-05-Authorization 授权

Shiro-06-Realms 领域

Shiro-07-Session Management 会话管理

Shiro-08-Cryptography 编码加密

Shiro-09-web 整合

Shiro-10-caching 缓存

Shiro-11-test 测试

Shiro-12-subject 主体

Shiro-20-shiro 整合 spring 实战及源码详解

Shiro-21-shiro 整合 springmvc 实战及源码详解

Shiro-22-shiro 整合 springboot 实战

Shiro-30-手写实现 shiro

Shiro-31-从零手写 shiro 权限校验框架 (1) 基础功能

Realm(领域)

领域是可以访问特定于应用程序的安全性数据(例如用户,角色和权限)的组件。

Realm将此特定于应用程序的数据转换为Shiro可以理解的格式,因此Shiro可以反过来提供单个易于理解的Subject编程API,无论存在多少数据源或您的数据有多少特定于应用程序。

领域通常与数据源(例如关系数据库,LDAP目录,文件系统或其他类似资源)具有一对一的关联。

这样,Realm接口的实现使用特定于数据源的API来发现授权数据(角色,权限等),例如JDBC,文件IO,Hibernate或JPA或任何其他数据访问API。

因为这些数据源中的大多数通常都存储身份验证数据(密码等凭据)以及授权数据(例如角色或权限),所以每个Shiro Realm都可以执行身份验证和授权操作。

领域配置

如果使用Shiro的INI配置,则您可以像[main]部分中的任何其他对象一样定义和引用领域,但是它们是通过以下两种方式之一在securityManager上配置的:显式或隐式。

显式分配

根据到目前为止的INI配置知识,这是一种显而易见的配置方法。

定义一个或多个领域之后,可以将它们设置为securityManager对象上的集合属性。

例如:

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

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

显式分配是确定性的-您可以精确控制使用哪些领域以及它们用于身份验证和授权的顺序。

领域顺序的影响在“身份验证”一章的“身份验证序列”部分中进行了详细说明。

隐式分配

  • 不喜欢

如果您更改领域的定义顺序,则隐式分配可能会导致意外行为。

建议您避免使用这种方法,而应使用具有确定性行为的显式分配。

隐式分配很可能在以后的Shiro版本中被弃用/删除。

如果由于某种原因您不想显式配置securityManager.realms属性,则可以允许Shiro检测所有已配置的领域并将它们直接分配给securityManager。

使用这种方法,将按照定义的顺序将领域分配给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

但是,请意识到,使用隐式分配,仅定义领域的顺序会直接影响在身份验证和授权尝试期间如何查询它们。

如果您更改其定义顺序,则将更改主身份验证器的身份验证序列的功能。

因此,为了确保确定性行为,我们建议使用“显式分配”而不是“隐式分配”。

领域认证

了解Shiro的主身份验证工作流程后,重要的是要准确知道身份验证器在身份验证尝试期间与领域进行交互时会发生什么。

支持身份验证令牌

如认证序列中所述,在咨询Realm进行认证尝试之前,将调用其支持方法。如果返回值为true,则仅会调用其getAuthenticationInfo(token)方法。

通常,领域会检查所提交令牌的类型(接口或类),以查看其是否可以处理它。

例如,处理生物特征数据的领域可能根本无法理解UsernamePasswordTokens,在这种情况下,它将从supports方法返回false。

处理支持的 AuthenticationToken

如果领域支持提交的AuthenticationToken,那么Authenticator将调用该领域的getAuthenticationInfo(token)方法。这有效地表示了使用Realm的后备数据源进行的身份验证尝试。

方法,依次为:

  1. 检查用于标识主体的令牌(帐户标识信息)

  2. 根据委托人,在数据源中查找相应的帐户数据

  3. 确保令牌提供的凭证与数据存储中存储的凭证匹配

  4. 如果凭据匹配,则返回AuthenticationInfo实例,该实例以Shiro理解的格式封装帐户数据

  5. 如果凭据不匹配,则抛出AuthenticationException

这是所有Realm getAuthenticationInfo实现的最高级别的工作流程。

领域可以自由地在此方法期间做任何想做的事情,例如在审计日志中记录尝试,更新数据记录或其他对该数据存储进行身份验证尝试有意义的事情。

唯一需要做的就是,如果凭据与给定的主体匹配,则返回一个非空的AuthenticationInfo实例,该实例表示该数据源中的使用者帐户信息。

省时间小技巧

直接实现Realm接口可能很耗时且容易出错。

大多数人选择继承AuthorizingRealm抽象类,而不是从头开始。

此类实现通用的身份验证和授权工作流,以节省您的时间和精力。

凭证匹配

在上述领域身份验证工作流程中,领域必须验证主题的提交凭据(例如密码)必须与数据存储区中存储的凭据匹配。

如果它们匹配,则认为身份验证成功,并且系统已经验证了最终用户的身份。

领域凭证匹配

将提交的凭证与存储在其支持数据存储区中的凭证进行匹配是每个Realm的责任,而不是“身份验证器”的责任。

每个“领域”都对凭证格式和存储有深入的了解,并且可以执行详细的凭证匹配,而“身份验证器”是通用的工作流程组件。

凭证匹配过程在所有应用程序中几乎都是相同的,并且通常仅根据所比较的数据而有所不同。为确保此过程在必要时可插入和可自定义,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
...

SimpleCredentialsMatcher

Shiro的所有现成Realm实现默认都使用 SimpleCredentialsMatcher。

SimpleCredentialsMatcher使用AuthenticationToken中提交的内容对存储的帐户凭据执行简单的直接相等性检查。

例如,如果提交了UsernamePasswordToken,则SimpleCredentialsMatcher会验证提交的密码与存储在数据库中的密码完全相同。

不过,SimpleCredentialsMatcher不仅可以对字符串进行直接相等比较,还可以对字符串进行比较。

它可以与大多数常见的字节源一起使用,例如字符串,字符数组,字节数组,文件和InputStreams。

有关更多信息,请参见其JavaDoc。

散列凭证(Hashing Credentials)

除了以原始格式存储凭据并执行原始/普通比较之外,一种存储最终用户凭据(例如密码)的更为安全的方法是,在将凭据存储到数据存储区之前先对其进行单向哈希处理。

这样可以确保最终用户的凭据永远不会以原始格式存储,并且没有人知道原始/原始值。与纯文本或原始比较相比,这是一种更加安全的机制,并且所有注重安全性的应用程序都应优先于非哈希存储使用此方法。

为了支持这些首选的密码哈希策略,Shiro提供了要在领域上配置的HashedCredentialsMatcher实现,而不是前面提到的SimpleCredentialsMatcher。

散列凭证以及加盐和多次散列迭代的好处不在此Realm文档的范围之内,但一定要阅读HashedCredentialsMatcher JavaDoc,其中详细介绍了这些原理。

哈希和对应的匹配器

那么,如何配置启用Shiro的应用程序来轻松做到这一点?

Shiro提供了多个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来匹配您的哈希首选项。

在此示例中,我们创建了一个随机盐,并执行了1024次哈希迭代以增强安全性(有关原因,请参见HashedCredentialsMatcher JavaDoc)。

这是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接口确保HashedCredentialsMatcher可以引用您在创建用户帐户时使用的盐(例如,上面的user.setPasswordSalt(salt);调用)。

HashedCredentialsMatcher需要加盐才能对提交的AuthenticationToken执行相同的哈希技术,以查看令牌是否与您在数据存储区中保存的令牌匹配。

因此,如果您使用盐析作为用户密码(应该这样做!!!),请确保您的Realm实现通过返回SaltedAuthenticationInfo实例来表示。

禁用身份验证

如果由于某种原因,您不希望Realm对数据源执行身份验证(也许是因为您只希望Realm执行授权),则可以通过始终从Realm的support方法返回false来完全禁用Realm对身份验证的支持。

这样,在尝试进行身份验证时就永远不会查询您的领域。

当然,如果要验证主题,则至少需要配置一个Realm才能支持AuthenticationToken。

领域授权

SecurityManager将权限或角色检查任务委托给Authorizer,默认为ModularRealmAuthorizer。

基于角色的授权

在主题上调用重载方法hasRoles或checkRoles方法之一时

  1. 主题委托给SecurityManager以确定是否已分配给定角色

  2. 然后,SecurityManager委托给授权者

  3. 然后,授权者一一引荐所有授权领域,直到找到分配给该主题的给定角色。如果没有任何领域授予主题,则返回false来拒绝访问

  4. 授权领域AuthorizationInfo getRoles()方法以获取分配给Subject的所有角色

  5. 如果在AuthorizationInfo.getRoles调用返回的角色列表中找到给定的Role,则授予访问权限。

基于权限的授权

在主题上调用重载方法之一isPermitted()或checkPermission()方法时:

  1. 主题将任务委派给SecurityManager授予或拒绝权限

  2. 然后,SecurityManager委托给授权者

  3. 然后,授权者逐一引用所有授权者领域,直到授予权限为止。如果未由任何授权领域授予许可,则拒绝主题

  4. 授权领域执行以下操作以检查是否允许主题:

a、首先,它通过在AuthorizationInfo上调用getObjectPermissions()和getStringPermissions方法并汇总结果来直接标识分配给Subject的所有权限。

b、如果注册了RolePermissionResolver,则可通过调用RolePermissionResolver.resolvePermissionsInRole()来基于分配给Subject的所有角色检索Permissions。

C、对于来自a的汇总权限。和b。调用implies()方法以检查这些权限中的任何一个是否隐含已检查的权限。请参阅WildcardPermission

小结

希望本文对你有所帮助,如果喜欢,欢迎点赞收藏转发一波。

我是老马,期待与你的下次相遇。

参考资料

10 Minute Tutorial on Apache Shiro

https://shiro.apache.org/reference.html

https://shiro.apache.org/session-management.html