Shiro原始碼分析(3) - 認證器(Authenticator)
本文在於分析Shiro原始碼,對於新學習的朋友可以參考
[開濤部落格](http://jinnianshilongnian.iteye.com/blog/2018398)進行學習。
Authenticator就是認證器,在Shiro中負責認證使用者提交的資訊,在Shiro中我們用AuthenticationToken來表示提交的資訊。Authenticator介面只提供了一個認證的方法。如下。
/**
* 認證使用者提交的資訊AuthenticationToken物件,AuthenticationToken包含了身份和憑證。
* 如果認證成功則返回AuthenticationInfo物件,AuthenticationInfo物件代表了使用者在Shiro中已經被認證過的賬戶資料。
* 如果認證失敗則丟擲一下異常
* @see ExpiredCredentialsException 憑證過期
* @see IncorrectCredentialsException 憑證錯誤
* @see ExcessiveAttemptsException 多次嘗試失敗
* @see LockedAccountException 賬戶鎖定
* @see ConcurrentAccessException 併發訪問異常(多點登入)
* @see UnknownAccountException 賬戶未知
*/
public AuthenticationInfo authenticate (AuthenticationToken authenticationToken)
throws AuthenticationException;
AuthenticationToken分析
AuthenticationToken物件代表了身份和憑證。從下面的介面看提供的方法很簡單,但返回的物件都是Object,也就是說在Shiro中對身份和憑證的型別沒有限制,Shiro沒有提供特有的型別來處理。
public interface AuthenticationToken extends Serializable {
/**
* 獲取身份
*/
Object getPrincipal();
/**
* 獲取憑證
*/
Object getCredentials();
}
在Shiro中只提供了一種具體的實現類UsernamePasswordToken。UsernamePasswordToken類是以使用者名稱作為身份,密碼作為憑證。當然它也實現了RememberMeAuthenticationToken介面,提供rememberMe功能。rememberMe功能的實現在後面再分析,在這裡不是重點。UsernamePasswordToken很簡單,只有構造方法和setter/getter方法。我們需要對UsernamePasswordToken中的身份和憑證要有很好的理解,什麼可以作為身份,什麼又是憑證。
AuthenticationInfo分析
AuthenticationInfo表示被Subject儲存的賬戶,這個賬戶是經過認證的。而AuthenticationToken中的身份/憑證是使用者提交的資料,還沒有經過認證,如果認證成功才會被儲存在AuthenticationInfo中。
AuthenticationInfo只有一個實現類SimpleAuthenticationInfo(備註:SimpleAccount也是其中一個實現類,但功能是完全依賴SimpleAuthenticationInfo實現的)。AuthenticationInfo還有兩個子介面,分別是:SaltedAuthenticationInfo和MergableAuthenticationInfo。SaltedAuthenticationInfo提供了獲取憑證加密鹽的方法,MergableAuthenticationInfo可以合併驗證後的身份資訊。各自的介面都很簡單,下面直接分析SimpleAuthenticationInfo具體實現。
SimpleAuthenticationInfo詳細分析
下面是SimpleAuthenticationInfo的屬性和構造方法。下面有很多構造方法,但可以看出實現都依賴到SimplePrincipalCollection物件。SimplePrincipalCollection物件負責收集身份(principals)和域(realm)的關係。關於SimplePrincipalCollection我們暫時不展開分析。
/**
* 身份集合
*/
protected PrincipalCollection principals;
/**
* 憑證
*/
protected Object credentials;
/**
* 憑證鹽
*/
protected ByteSource credentialsSalt;
public SimpleAuthenticationInfo() {
}
public SimpleAuthenticationInfo(Object principal, Object credentials, String realmName) {
this.principals = new SimplePrincipalCollection(principal, realmName);
this.credentials = credentials;
}
public SimpleAuthenticationInfo(Object principal, Object hashedCredentials, ByteSource credentialsSalt, String realmName) {
this.principals = new SimplePrincipalCollection(principal, realmName);
this.credentials = hashedCredentials;
this.credentialsSalt = credentialsSalt;
}
public SimpleAuthenticationInfo(PrincipalCollection principals, Object credentials) {
this.principals = new SimplePrincipalCollection(principals);
this.credentials = credentials;
}
public SimpleAuthenticationInfo(PrincipalCollection principals, Object hashedCredentials, ByteSource credentialsSalt) {
this.principals = new SimplePrincipalCollection(principals);
this.credentials = hashedCredentials;
this.credentialsSalt = credentialsSalt;
}
在SimpleAuthenticationInfo中,我們主要分析一下merge(AuthenticationInfo info)方法,也就是說可以合併其他的AuthenticationInfo資訊。
public void merge(AuthenticationInfo info) {
// 判斷是否有身份資訊,如果沒有就返回
if (info == null || info.getPrincipals() == null || info.getPrincipals().isEmpty()) {
return;
}
// 合併身份集合
if (this.principals == null) {
this.principals = info.getPrincipals();
} else {
if (!(this.principals instanceof MutablePrincipalCollection)) {
this.principals = new SimplePrincipalCollection(this.principals);
}
((MutablePrincipalCollection) this.principals).addAll(info.getPrincipals());
}
// 憑證鹽只是在Realm憑證匹配過程中使用
// 如果存在憑證鹽,就不用管其他的了,如果沒有就使用其他的憑證鹽
if (this.credentialsSalt == null && info instanceof SaltedAuthenticationInfo) {
this.credentialsSalt = ((SaltedAuthenticationInfo) info).getCredentialsSalt();
}
// 合併憑證資訊
Object thisCredentials = getCredentials();
Object otherCredentials = info.getCredentials();
if (otherCredentials == null) {
return;
}
if (thisCredentials == null) {
this.credentials = otherCredentials;
return;
}
// 使用集合來合併憑證
if (!(thisCredentials instanceof Collection)) {
Set newSet = new HashSet();
newSet.add(thisCredentials);
setCredentials(newSet);
}
Collection credentialCollection = (Collection) getCredentials();
if (otherCredentials instanceof Collection) {
credentialCollection.addAll((Collection) otherCredentials);
} else {
credentialCollection.add(otherCredentials);
}
}
AbstractAuthenticator抽象類
和AbstractSessionManager一樣,AbstractAuthenticator主要功能也是提供監聽器,對認證過程中的狀態進行監聽。在認證過程中監聽成功、失敗、登出情況。監聽器是AuthenticationListener,下面看一下監聽器提供的方法。
public interface AuthenticationListener {
/**
* 監聽認證成功
*/
void onSuccess(AuthenticationToken token, AuthenticationInfo info);
/**
* 監聽認證失敗
*/
void onFailure(AuthenticationToken token, AuthenticationException ae);
/**
* 監聽使用者登出
*/
void onLogout(PrincipalCollection principals);
}
另外,AbstractAuthenticator實現了Authenticator#authenticate(AuthenticationToken token),處理了對監聽器通知的情況,但執行認證的具體過程提供抽象方法doAuthenticate(AuthenticationToken token)讓子類完成。
public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
// Token引數異常
if (token == null) {
throw new IllegalArgumentException("Method argumet (authentication token) cannot be null.");
}
AuthenticationInfo info;
try {
// 執行認證過程的抽象方法,子類去實現
info = doAuthenticate(token);
if (info == null) {
String msg = "No account information found for authentication token [" + token + "] by this " +
"Authenticator instance. Please check that it is configured correctly.";
throw new AuthenticationException(msg);
}
} catch (Throwable t) {
AuthenticationException ae = null;
if (t instanceof AuthenticationException) {
ae = (AuthenticationException) t;
}
if (ae == null) {
String msg = "Authentication failed for token submission [" + token + "]. Possible unexpected " +
"error? (Typical or expected login exceptions should extend from AuthenticationException).";
ae = new AuthenticationException(msg, t);
}
try {
// 認證失敗了,通知監聽器
notifyFailure(token, ae);
} catch (Throwable t2) {
}
throw ae;
}
// 認證成功,通知監聽器
notifySuccess(token, info);
return info;
}
ModularRealmAuthenticator類分析
在Shiro中只提供了一個具體的實現類ModularRealmAuthenticator,該類可以處理多個Realm的認證方式。
在ModularRealmAuthenticator中,把認證的權利交給域(Realm)去完成,在Shiro中Realm相當於資料的來源,可以自定義。ModularRealmAuthenticator支援多個Realm進行認證,在多個Realm認證時,需要設定認證策略,策略介面是AuthenticationStrategy。在Shiro中提供了三種認證策略。分別是:
- AllSuccessfulStrategy:所有Realm認證成功。
- AtLeastOneSuccessfulStrategy:至少有一個Realm認證成功。
- FirstSuccessfulStrategy: 第一個Realm認證成功。
關於認證策略我們在後面在分析,現在繼續分析ModularRealmAuthenticator。我們還是從屬性和構造方法分析。
// 認證的過程交由Realm去處理
private Collection<Realm> realms;
// 指定認證策略
private AuthenticationStrategy authenticationStrategy;
public ModularRealmAuthenticator() {
// 預設提供策略:至少有一個Realm認證成功就算認證成功
this.authenticationStrategy = new AtLeastOneSuccessfulStrategy();
}
下面我看看ModularRealmAuthenticator是如何實現doAuthenticate(token)方法的。
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
// 判斷realms屬性,必須要有Realm
assertRealmsConfigured();
Collection<Realm> realms = getRealms();
// 分支:如果只有一個就按照單個流程處理,如果有多個Realm就按照多個流程走認證策略。
if (realms.size() == 1) {
return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
} else {
return doMultiRealmAuthentication(realms, authenticationToken);
}
}
// 處理單個Realm
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
// 判斷realm是否支援處理token
if (!realm.supports(token)) {
String msg = "Realm [" + realm + "] does not support authentication token [" +
token + "]. Please ensure that the appropriate Realm implementation is " +
"configured correctly or that the realm accepts AuthenticationTokens of this type.";
throw new UnsupportedTokenException(msg);
}
// realm處理認證過程,處理過程中可能會丟擲認證異常AuthenticationException。
// 如果認證成功info不會用null。
AuthenticationInfo info = realm.getAuthenticationInfo(token);
if (info == null) {
String msg = "Realm [" + realm + "] was unable to find account data for the " +
"submitted AuthenticationToken [" + token + "].";
throw new UnknownAccountException(msg);
}
return info;
}
// 處理多個Realm
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
AuthenticationStrategy strategy = getAuthenticationStrategy();
// 返回一個空的聚合物件
// AllSuccessfulStrategy - 返回空的SimpleAuthenticationInfo物件
// AtLeastOneSuccessfulStrategy - 返回空的SimpleAuthenticationInfo物件
// FirstSuccessfulStrategy - 返回null
AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
for (Realm realm : realms) {
// 認證前處理
// AllSuccessfulStrategy - 判斷realm.supports(token),如果不支援直接拋異常,返回aggregate
// AtLeastOneSuccessfulStrategy - 返回aggregate
// FirstSuccessfulStrategy - 返回aggregate,也就是null
aggregate = strategy.beforeAttempt(realm, token, aggregate);
if (realm.supports(token)) {
AuthenticationInfo info = null;
Throwable t = null;
try {
//認證過程是由Realm處理的
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);
}
}
// 認證後處理,
// AllSuccessfulStrategy - 如果有異常會丟擲異常, 如果沒有就合併info和aggregate
// AtLeastOneSuccessfulStrategy - 如果有異常並不會丟擲,只是會合併info和aggregate
// FirstSuccessfulStrategy - 如果aggregate存在,則返回aggregate;否則返回info
aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);
} else {
log.debug("Realm [{}] does not support token {}. Skipping realm.", realm, token);
}
}
// AllSuccessfulStrategy - 返回aggregate
// AtLeastOneSuccessfulStrategy - 判斷aggregate不為空,否則拋異常
// FirstSuccessfulStrategy - 返回aggregate
aggregate = strategy.afterAllAttempts(token, aggregate);
return aggregate;
}
通過對上面的程式碼分析:
- AllSuccessfulStrategy策略流程:逐一處理Realm,每個Realm必須支援token處理,然後合併AuthenticationInfo。如果遇到異常,則丟擲異常結束迴圈。
- AtLeastOneSuccessfulStrategy策略流程:逐一處理Realm,不支援處理token的Realm跳過。如果遇到異常,忽略對異常的處理。對於認證通過的AuthenticationInfo進行合併成aggregate,最後判斷aggregate,aggregate不能為空,如果有空丟擲異常。
- FirstSuccessfulStrategy策略流程:逐一處理Realm,不支援處理token的Realm跳過。如果遇到異常,忽略對異常的處理。在迴圈處理Realm前aggregate=null,重點是在strategy.afterAttempt(realm, token, info, aggregate, t)的處理上,並不會合併info和aggregate。如果aggregate為空,則返回info。所以在處理後返回的總是第一個認證成功的AuthenticationInfo。
總結
Authenticator負責對AuthenticationToken進行認證,然後返回一個已經被認證過的資訊AuthenticationInfo。Authenticator也提供了監聽器AuthenticationListener,對認證狀態進行監聽。Authenticator真實的認證過程是由Realm來處理的,可以支援都多個Realm來認證,認證的過程中可以選擇不同的認證策略。