Apache Shiro Realms 介绍
Realm
是负责获取应用程序安全相关的数据
(如用户,角色,权限),并将其转化为Shiro理解的格式的组件,正是因为它,Shiro才可以提供一个统一的,好用的Subject
编程API,而不需要关心存储安全相关数据的数据源是什么形式的
通常Realm
和不同的数据源(关系型数据库,LDAP,文件系统
等)之间是一对一的关系,Realm的实现会使用各个数据源特定的API来获取数据,如JDBC,JPA,文件 IO
等等
Realm本质上就是专用于安全方面的DAO
应用程序一般会把认证数据和授权数据存储在同一个数据源之中,因此,Realm
拥有认证和授权两个功能
Realm 配置
如果使用的是Shiro的INI配置,可以像其他组件那样在 [main]
小节中进行配置,但是有两种不同方式
显式配置
可以直接给SecurityManager
的realms
赋值,realms
是一个集合类型的属性
fooRealm = com.company.foo.Realm
barRealm = com.company.another.Realm
bazRealm = com.company.baz.Realm
securityManager.realms = $fooRealm, $barRealm, $bazRealm
这种方式配置后,Realm
的顺序是确定的,在认证和授权的过程中,SecurityManager
将会按照这个顺序与所有的Realm
进行交互,这个顺序是很重要的,详情看认证
章节中的认证流程
小节
隐式配置
不推荐使用,隐式配置中,Realm的顺序取决于各个Realm出现的顺序,或许不经意间修改了Realm的顺序,使得认证和授权过程中出现问题,容易出错。在以后的版本中将会删除这种隐式配置
Shiro
可以智能的检测所有配置的Realms
,并将它们交给securityManager
,这种方式将会让以Realm
被定义的顺序作为以后SecurityManager
与其交互的顺序,如下面的例子:
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
然而,这种方式不推荐使用,原因在前面,更推荐采用显式配置的方式,因为它有一个确定的顺序
Realm 认证
现在来了解一下当Authenticator
与Realm
交互时发生了什么
检查是否支持AuthenticationToken
正如在认证那一章节所描述的,在一个Realm
执行认证之前,将会调用它的supports
方法来检查传过来的AuthenticationToken
是否是这个Realm能够处理的类型。只有当它返回true
时,才会调用它的getAuthenticationInfo(token)
方法
处理AuthenticationToken
如果Realm
能够处理传过来的AuthenticationToken
,那么Authenticator
将会调用Realm
的getAuthenticationInfo(token)
方法,该方法从后端数据源中找到相关的认证信息同时进行认证。其流程为:
- 从
AuthenticationToken
中获取需要认证的用户principal
,即账号 - 基于上面的
principal
,从数据源中找到相关的账户信息 - 将
AuthenticationToken
中的credentials
与存储在数据源中的进行对比,即密码匹配 - 如果匹配的上,将会将账户信息封装为一个
AuthenticationInfo
实例返回给Shiro
- 如果没匹配上,则会抛出一个
AuthenticationException
以上流程是getAuthenticationInfo
方法的概括,实际上,Realms
可以做任何你想在其中做的事,比如日志记录等等。如果creadentials
匹配无误,那么将会返回一个非空的AuthenticationInfo
实例供Subject使用
直接实现一个Realm接口是比较复杂且困难的,因此Shiro提供了一个AuthorizingRealm抽象类,它实现了一个通用的认证和授权的工作流程,可以继承它来自定义Realm以节约时间
Credentials 匹配
如果按照上面的描述,Realm
将负责校验Subject
提交的credentials
(如密码)是否与存储在数据源中的一致,如果它们匹配的上,那么认证成功
注意:
credentials
匹配是Realm
的职责,而不是Authenticator
的职责。Realm
知道credentials
的格式,可以执行详细的credentials
匹配,而Authenticator
只是一个通用的工作流的组件
credentials
的匹配在所有的应用程序中都几乎一致,而只有详细的数据比对时会有差别,因此为了让这个过程可定制化,AuthenticatingRealm
及其子类都支持CredentialsMatcher
的概念,它专门用来执行credentials
比对逻辑
从数据源中找到相关账号信息以后,这些信息将会和Subject
提交的AuthenticationToken
一起交给CredentialsMatcher
来进行credentials
的比对
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会使用SimpleCredentialsMatcher
,它进行的仅仅是一个简单的文本匹配,比如提交的是一个UsernamePasswordToken
,那么SimpleCredentialsMatcher
将仅仅比对token
中的credentials
和数据源中存储的credentials
是否相等
SimpleCredentialsMatcher
不仅可以比对字符串,也可以比对字符数组,字节数组,文件和InputStreams等
散列的credentials
目前更安全的方式是将用户的credentials
进行单向散列处理后,存储到数据源中,这确保了用户credentials
的安全性,没人能知道它的原始值是多少
为了支持这种方式,Shiro提供了HashedCredentialsMatcher
来进行credentials
比对。相关详细信息可以查看HashedCredentialsMatcher
文档
散列和比对
Shiro提供了各种HashedCredentialsMatcher
实现,我们需要指定使用哪个实现类来对你的应用程序中的用户credentials
进行散列
假设如下场景:你的应用程序使用用户名/密码来进行认证,你将使用SHA-256
算法来对用户密码进行单向散列,然后存储到数据源中,那么你可能会使用如下的代码
import org.apache.shiro.crypto.hash.Sha256Hash;
import org.apache.shiro.crypto.RandomNumberGenerator;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
...
// 这里使用随机数生成器来生成盐
// 正常应用将会专门用一个属性来作为盐
RandomNumberGenerator rng = new SecureRandomNumberGenerator();
Object salt = rng.nextBytes();
// 现在我们使用这个随机生成的盐,多次迭代,然后得到一个base64编码的值
String hashedPasswordBase64 = new Sha256Hash(plainTextPassword, salt, 1024).toBase64();
User user = new User(username, hashedPasswordBase64);
// 将盐保存在账号信息中,HashedCredentialsMatcher也会使用这个盐来进行密码比对
user.setPasswordSalt(salt);
userDAO.create(user);
因为采用的是SHA-256
算法对密码进行了加密,因此也需要告诉Shiro使用哪个HashedCredentialsMatcher
来进行密码比对。这个例子中,我们使用的是随机盐,执行了1024次迭代,因此,相关配置如下:
[main]
...
credentialsMatcher = org.apache.shiro.authc.credential.Sha256CredentialsMatcher
# base64 encoding, not hex in this example:
credentialsMatcher.storedCredentialsHexEncoded = false
credentialsMatcher.hashIterations = 1024
# 下面这个属性仅仅在Shiro1.0需要,在1.1及之后的版本中移除了(文档上的描述应该是错的)
credentialsMatcher.hashSalted = true
...
myRealm = com.company.....
myRealm.credentialsMatcher = $credentialsMatcher
...
SaltedAuthenticationInfo
最后需要注意的是,这里Realm
必须返回一个SaltedAuthenticationInfo
的实例。SaltedAuthenticationInfo
接口确保了你使用的盐能被HashedCredentialsMatcher
感知到
HashedCredentialsMatcher
需要同样的盐,对Subject
提交的AuthenticationToken
执行同样的加密算法才能知道是否匹配。因此,如果对用户密码在存储时进行了加盐加密,那就需要Realm
在认证时返回一个SaltedAuthenticationInfo
的实例
禁用认证
如果你不想让Realm
执行认证的逻辑(或许仅仅只想要Realm执行授权的逻辑),可以通过修改supports
方法,让其直接返回一个false
,即可禁用掉该realm
的认证功能。当然,应用程序中至少需要一个Realm
得负责认证
Realm 授权
SecurityManager
会将检查权限和角色的任务交给Authorizer
组件,默认是ModularRealmAuthorizer
基于角色的授权
当Subject
的hasRoles()
方法或checkRoles()
方法被调用时:
-
Subject
将需要的Role
交给SecurityManager
进行检查 -
SecurityManager
会把工作交给Authorizer
-
Authorizer
会遍历所有的Authorizing Realms
,调用其hasRoles()
方法和checkRoles()
方法,直到有Realm
返回true
时,权限检查通过。否则就是没有权限 -
Authorizing Realm
会通过AuthorizationInfo的getRoles()
方法来获取Subject
拥有的所有角色 - 如果在
AuthorizationInfo.getRoles()
返回的Role
列表中包含需要的Role
,则允许访问
基于权限的授权
当Subject
的isPermitted()
方法或checkPermission()
方法被调用时
-
Subject
将委托SecurityManager
执行权限检查 -
SecurityManager
委托给Authorizer
-
Authorizer
会遍历所有的Authorizing Realm
,调用其isPermitted()或checkPermission()
方法,直到有Realm
返回true时,权限检查通过。否则Subject
就是没有权限 -
Authorizing Realm
会以下面的流程来检查权限:- 调用
AuthorizationInfo
的getObjectPermissions()
和getStringPermissions()
先获取所有Subject拥有的权限并聚合结果 - 如果注册了
RolePermissionResolver
,则它也会被用于遍历该Subject所有基于角色的权限,通过调用RolePermissionResolver.resolvePermissionsInRole()
- 遍历上面两步得到的权限,调用其
implies()
方法,检查是否其隐含了我们正在检查的权限。如"user:*
"隐含了"user:delete
"权限
- 调用