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"权限
- 调用