之前在Authentication和Authorization中也提到Realm。
無論是身份驗證還是權限驗證,無論數據以什么方式存在,我們都需要訪問一些數據並將其轉換為Shiro可以識別的格式。
通常一個數據源對應一個Realm。因此,實現一個Realm時會用到該數據源相關的API。
通常一個數據源中會同時保存身份相關數據與權限相關數據。因此,一個Realm實現類可以進行認證和授權兩種操作。
可以將Realm簡單地理解為DAO。
(雖然IDEA生成的type hirarchy diagram很漂亮,但是太大了...還是用回eclipse截圖..)
如果使用.ini配置,我們可以在[main]部分定義N個Realm,但我們可以用顯式(explicit)和隱式(implicit)兩種方式為securityManager指定Realm(有點IOC容器的意思)。
顯示指定就是常見的方式,即定義Realm后再為securityManager按需要的順序指定Realm。
fooRealm = com.company.foo.Realm
barRealm = com.company.another.Realm
bazRealm = com.company.baz.Realm
securityManager.realms = $fooRealm, $barRealm, $bazRealm
顯示指定的方式相對較為明確,即使不改變Realm的定義,我們仍可以讓驗證和授權按我們給securityManager指定的順序的Realm來執行。
如果因為某些原因(可能是定義的Realm太多?)不想為securityManager.realms指定,我們也可以使用隱式方式。
也就是說,把上面的配置改成如下形式就是隱式方式了:
blahRealm = com.company.blah.Realm
fooRealm = com.company.foo.Realm
barRealm = com.company.another.Realm
隱式方式其實就是不指定,只定義(define),Shiro會搜索配置中所有的Realm並將它們一一指定給securityManager。
使用隱式方式時只要稍微改一下Realm的定義,Shiro就可能會給我們來個驚喜。
在介紹Authentication的文章中,說的是當一個驗證請求出現時Shiro框架的工作流程。
在這里具體記錄一下Realm負責的工作(雖然說securiyManager驗證開始的地方,但從數據源取數據並作比較的工作是由Realm來進行的)。
以ModularRealmAuthenticator為例:
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
AuthenticationStrategy strategy = getAuthenticationStrategy();
AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
if (log.isTraceEnabled()) {
log.trace("Iterating through {} realms for PAM authentication", realms.size());
}
for (Realm realm : realms) {
aggregate = strategy.beforeAttempt(realm, token, aggregate);
if (realm.supports(token)) {
log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm);
AuthenticationInfo info = null;
Throwable t = null;
try {
info = realm.getAuthenticationInfo(token);
} catch (Throwable throwable) {
t = throwable;
if (log.isDebugEnabled()) {
String msg = "Realm [" + realm + "] threw an exception during a multi-realm authentication attempt:";
log.debug(msg, t);
}
}
aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);
} else {
log.debug("Realm [{}] does not support token {}. Skipping realm.", realm, token);
}
}
aggregate = strategy.afterAllAttempts(token, aggregate);
return aggregate;
}
在Realm開始處理驗證的邏輯之前,Authenticator將調用Realm的supports方法去驗證當前Realm是否支持獲得的AuthenticationToken。
通常,Realm檢查的是token的類型,比如在AuthenticatingRealm中檢查類型是否相同。
public boolean supports(AuthenticationToken token) {
return token != null && getAuthenticationTokenClass().isAssignableFrom(token.getClass());
}
另外,AuthenticatingRealm的constructor中類型默認為
authenticationTokenClass = UsernamePasswordToken.class;
如果當前Realm支持提交過來的token,authenticator則調用getAuthenticationInfo(token)方法。
以AuthenticatingRealm為例(注意是final):
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info = getCachedAuthenticationInfo(token);
if (info == null) {
//otherwise not cached, perform the lookup:
info = doGetAuthenticationInfo(token);
log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
if (token != null && info != null) {
cacheAuthenticationInfoIfPossible(token, info);
}
} else {
log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
}
if (info != null) {
assertCredentialsMatch(token, info);
} else {
log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);
}
return info;
}
如果可以從緩存中獲得驗證信息,下一步則檢查密碼是否匹配,即assertCredentialsMatch(token, info)。
如果緩存中不存在驗證信息則調用以下方法。
protected abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;
這里我們暫時先不考慮緩存的情況,考慮doGetAuthenticationInfo應該做什么。
有些人(比如我)直接在該方法中完成也驗證,驗證通過時返回SimpleAuthenticationInfo實例,失敗則拋出相應的驗證異常。
但下面有個assertCredentialsMatch,說明doGetAuthenticationInfo本沒有打算這樣用,這種使用方式會讓CredentialMatcher失去意義。
參考JdbcRealm的實現,只是根據身份(用戶名)去查詢並返回SimpleAuthenticationInfo實例。
然后讓assertCredentialsMatch比較token和authenticationInfo。
protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
CredentialsMatcher cm = getCredentialsMatcher();
if (cm != null) {
if (!cm.doCredentialsMatch(token, info)) {
//not successful - throw an exception to indicate this:
String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials.";
throw new IncorrectCredentialsException(msg);
}
} else {
throw new AuthenticationException("A CredentialsMatcher must be configured in order to verify " +
"credentials during authentication. If you do not wish for credentials to be examined, you " +
"can configure an " + AllowAllCredentialsMatcher.class.getName() + " instance.");
}
}
既然提到了CredentialMatcher,我們來看看他的意義所在。 首先說明,AuthenticatingRealm的默認CredentialMatcher是...
public AuthenticatingRealm() {
this(null, new SimpleCredentialsMatcher());
}
如果僅僅是做密碼字符比較我們大可不必做出這樣一個接口(字符串比較的可插拔+可定制么?)
之前在說Authentication的時候就提過,AuthenticationToken的Principal和Credential可以是任何類型的,光是拿過來直接比較是否相符也不只是比較密碼字符那么簡單了。
SimpleCredentialsMatcher就是用來比較兩個credential是否相同的。
其doCredentialsMatch方法返回其equals方法的返回值。
protected boolean equals(Object tokenCredentials, Object accountCredentials) {
if (log.isDebugEnabled()) {
log.debug("Performing credentials equality check for tokenCredentials of type [" +
tokenCredentials.getClass().getName() + " and accountCredentials of type [" +
accountCredentials.getClass().getName() + "]");
}
if (isByteSource(tokenCredentials) && isByteSource(accountCredentials)) {
if (log.isDebugEnabled()) {
log.debug("Both credentials arguments can be easily converted to byte arrays. Performing " +
"array equals comparison");
}
byte[] tokenBytes = toBytes(tokenCredentials);
byte[] accountBytes = toBytes(accountCredentials);
return Arrays.equals(tokenBytes, accountBytes);
} else {
return accountCredentials.equals(tokenCredentials);
}
}
當然,實現類不只是SimpleCredentialsMatcher...
SimpleCredentialsMatcher下還跟着HashedCredentialsMatcher,再往下就都deprecated了。
說到HashedCredentialsMatcher,他只是給密碼加個salt以提高安全。
其中hashSalted屬性基本不用考慮,因為從Shiro 1.1開始salt是根據SaltedAuthenticationInfo的getCredentialSalt()方法返回的non-null value。
public HashedCredentialsMatcher() {
this.hashAlgorithm = null;
this.hashSalted = false;
this.hashIterations = 1;
this.storedCredentialsHexEncoded = true; //false means Base64-encoded
}
說到salting就不得不說SaltedAuthenticationInfo,該接口繼承AuthenticationInfo,即除了principal和credential,他還有一個credentialsSalt。
public interface SaltedAuthenticationInfo extends AuthenticationInfo {
/**
* Returns the salt used to salt the account's credentials or {@code null} if no salt was used.
*
* @return the salt used to salt the account's credentials or {@code null} if no salt was used.
*/
ByteSource getCredentialsSalt();
}
HashedCredentialsMatcher有個默認的salt,是將自己的principal作為salt,后來這個方法也被deprecated了。
因為從1.1開始,Shiro禁止salt從用戶的登錄信息中獲取,而應該從數據源獲取。
@Deprecated
protected Object getSalt(AuthenticationToken token) {
return token.getPrincipal();
}
這一系列方法和屬性會從Shiro 2.0開始徹底消失。