与用户安全相关的数据,如Users、Roles、Permissions等信息可能位于各种数据源中,如文件系统、LDAP、各种数据库等。Shiro中的Realm相当于是DAO层,对于低层的数据源而言,它调用相应的接口API获取数据,对于Shiro中的其它组件如Authenticator与Authorizer而言,Realm为它们提供统一的接口,Realm作用就是屏蔽不同的数据源,为上层的组件提供统一的接口。
1、Realm的配置
这个话题前边已讲说过了。首先Shiro支持多Realm,在身份认证与授权的过程中,Realm生效的顺序会影响到结果,而Realm生效的顺序是在配置文件中决定的。有两种,一种是显式顺序,如下:
fooRealm = com.company.foo.Realm
barRealm = com.company.another.Realm
bazRealm = com.company.baz.Realm
securityManager.realms = $fooRealm, $barRealm, $bazRealm
有三个Realm,它们生效的顺序取决于最后一行代码。
另一种是隐式顺序,如下:
fooRealm = com.company.foo.Realm
barRealm = com.company.another.Realm
bazRealm = com.company.baz.Realm
# securityManager.realms = $fooRealm, $barRealm, $bazRealm
最后一行被注释了,那么这个时候默认的生效顺序就是它们出现在配置文件中的顺序。
2、Realm与Authentication
看一下单个Realm与Authentication组件之间具体发生了那些事情。
2.1.是否支持AuthenticationTokens
首先Subject会将用户提交的Principal、Crendential转换成某种类型的AuthenticationTokens实现。AuthenticationTokens可以有多种实现,有的转换的可能是“用户名+密码”,有的可能转换的是“用户名+密码+验证码”,也有可能是“手机号+验证码等”很多很多。
Authenticator组件在使用某个Realm时,首先调用它的support方法,并将AuthenticationTokens传进去,Realm用instance of确认一下AuthenticationTokens的类型,比如Subject携带的是“用户名+密码”的AuthenticationTokens,而当前Realm只支持“手机号+验证码”的AuthenticationTokens,这个时候当前Realm就会返回false,Authenticator就会将当前Realm忽略,转而去查询其它的Realm。AuthenticationTokens其实在多Realm的情况下,起到过虑Realm的作用。
2.2.处理支持的AuthenticationTokens
如果当前Realm支持传入的AuthenticationTokens实例的类型,则后续的认证处理将会展开,Authenticator将会调用当前Realm的getAuthenticationInfo(token)方法,这个方法大致执行如下几个步骤:
- 检查token中的Principal,比如用户名,首先要确认这个用户名在数据源中是否存。
- 根据Principal从数据源获取其它内容,比如Principal是用户名,那么就用这个用户名查询相对的password。
- 将token中的Crendential与数据源中Crendential进行对比。
- 如果匹配,AuthenticationInfo实例被返回,里边封装用户相关数据。
- 如果不匹配就抛出异常。
对于开发者自定义的Realm,方法getAuthenticationInfo(token)只要最终返回AuthenticationInfo表示成功,抛出异常表示失败就可以了,内部实现细节由开发者自己决定。
提示:自定义的话一般从AuthorizingRealm抽象类派生,而不是全部从头开始,里边的默认实现可以节约开发者时间。
2.3.Crendential的匹配
在比对Crendential这一步中,不同的应用,逻辑可能是不同的。数据源中的Crendential可能是加秘的或者用什么算法编过码,用户提交的Crendential可能是加过盐的。因此在比对之前,Realm要分别对来自安全数据源的数据与用户提交的数据进行各种解秘、净化、转码之类的工作。因此Realm有一个CredentialsMatcher类型的接口,专门负责处理这件事情。用户可以实现自己的CredentialsMatcher并配置,如下:
[main]
...
customMatcher = com.company.shiro.realm.CustomCredentialsMatcher
myRealm = com.company.shiro.realm.MyRealm
myRealm.credentialsMatcher = $customMatcher
...
2.3.1.明文对比
以前的文章中提到过,Shiro内置了多种CredentialsMatcher的实现,比如HashedCredentialsMatcher、Md5CredentialsMatcher、Sha512CredentialsMatcher等。
Realm默认的CredentialsMatcher实现是SimpleCredentialsMatcher实现,只是对比用户提交的Crendential与安全数据源的Credential是否相同,不进行任何转换。
2.3.2.HASH运算
实际应用中,不能将用户的Crendential如password等直接明文保存在数据源中。一种常用的方法是将用户密码进行HASH运算,然后再将结果保存到数据源中。对于用户提交的password,也同样进行相同的运算,然后再与数据源中的HASH值进行对比。这样子除了用户,没有人知道他的密码是什么,因为数据源中保存的是HASH值,而由HASH值不可以逆向推导出用户的密码。但是如何用户的密码太简单太短的,有可能会被暴力破解。因此可以对用户的原始密码加盐与多重HASH计算提高暴力破解的难度。
当然对于用户登录时提交的凭证也要进行一样的运算,再去比对。下面是一个例子:
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);
上边代码演示的是如何创建用户,对用户密码加盐,HASH运算、编码最后保存的例子。
首先生成一个随机数当作是“盐”。
然后将用户的原始密码、生成的“盐”进行Sha256Hash运算,后边的1024表示迭代次数
然后进行base64编码。
最后在数据库中保存用户信息,包括刚才的盐。
这样的话,即使用户的密码很短很简单,并且用户的所有数据都泄露了,拿到这些数据的人也很难计算出用户的原始密码,这个需要庞大的计算力。
接下来看Shiro的Realm怎么配:
[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
...
- 在上边创建用户的例子了,为密码加了盐,在实际开发中,盐与用户信息往往保存在不同的地方,单纯的盐泄露与用户信息泄露都是安全的,提高安全度。所以对于自定义的myRealm,要实现里边的SaltedAuthenticationInfo接口,自定义获得盐的方式。
3、Disabling Authentication
指禁止掉某个Realm的身份认证功能,原因可能只是想将某个Realm当成授权处理的数据源。只要让这个Realm的support方法一直返回false就可以了。
4、Realm与Authorization
基于角色的授权:
- Subject将授权请求托管给SecurityManager
- SecurityManager再将请求托管给Authorizer组件。
- Authorizer逐个查找Realm,直到在某个Realm中找到匹配的角色,并返回true。如果查找了全部Realm以后也没有为当前Subject找到Role,则返回false。
- 对于每个Realm,它自己要从自己的数据源返回当前Subject的所有Role
- 如果没有角色,或者有角色但不匹配,返回false,否则返回true。
基于权限的授权:
- Subject将授权请求托管给SecurityManager
- SecurityManager再将请求托管给Authorizer组件。
- Authorizer逐个查找Realm,直到在某个Realm中找到匹配的权限,并返回true。如果查找了全部Realm以后也没有为当前Subject找到匹配的权限,则返回false。
- 对于Realm则执行如下步骤:
a.通过调用getObjectPermissions()方法获取Subject的全部权限,然后调用AuthorizationInfo的getStringPermissions方法聚合返回结果。
b.如果Realm被配置了RolePermissionResolver,则调用其RolePermissionResolver.resolvePermissionsInRole()方法,这个方法将与Subject对应的所有Role的所有Permission转换成标准格式。
c.调用 implies()方法检查权限,参考WildcardPermission。