寫在前面
在上一篇文章當中,我們通過一個簡單的例子,簡單地認識了一下shiro。在這篇文章當中,我們將通過閱讀原始碼的方式瞭解shiro的認證流程。
建議大家邊讀文章邊動手除錯程式碼,這樣效果會更好。
認證異常分析
shiro中的異常主要分為兩類,一類是AuthenticationException
認證異常,一類是AuthorizationException
許可權異常。分別對應http響應狀態碼中的401
和403
認證異常AuthenticationException子類
許可權異常AuthorizationException子類
當認證不通過時將根據具體情況丟擲AuthenticationException的子類,當鑑權不通過時將會丟擲AuthorizationException的子類。
我們通過檢驗shiro是否丟擲異常,從而判斷登入物件是否通過認證、是否具備相關保護資源的訪問許可權。
這也是我們在上一節的例子中,需要捕獲相關異常的原因。
接下來,我們通過閱讀原始碼的方式來分析一下shiro框架的認證流程。
認證流程分析
/**認證器
* @author 賴柄灃 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/9/21 0:50
*/
public class Authenticator {
private DefaultSecurityManager securityManager;
public Authenticator(){
//1. 建立安全管理器
this.securityManager = new DefaultSecurityManager();
//2. 給安全管理器設定問題域
//因為許可權資訊從ini檔案中讀取,所以是IniRealm
this.securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
//3. 注入安全管理器,並使用SecurityUtils全域性安全工具類完成認證
SecurityUtils.setSecurityManager(securityManager);
}
/**認證
* @author 賴柄灃 bingfengdev@aliyun.com
* @date 2020-09-23 16:22:11
* @param username 使用者名稱
* @param password 密碼
* @return void
* @version 1.0
*/
public void authenticate(String username,String password){
//4. 獲取當前主題
Subject subject = SecurityUtils.getSubject();
//5.根據登入物件身份憑證資訊建立登入令牌
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
//6.認證
//如果認證通過,則不丟擲異常,否則丟擲AuthenticationExceptixon異常子類
//正式專案建議直接丟擲,統一異常處理
try {
subject.login(token);
}catch (IncorrectCredentialsException e) {
e.printStackTrace();
}catch (ConcurrentAccessException e){
e.printStackTrace();
}catch (UnknownAccountException e){
e.printStackTrace();
}catch (ExcessiveAttemptsException e){
e.printStackTrace();
}catch (ExpiredCredentialsException e){
e.printStackTrace();
}catch (LockedAccountException e){
e.printStackTrace();
}
}
}
這是上一個例子當中的認證器的程式碼。 我們在上述程式碼的44行,shiro認證的入口處打個斷點,以便跟蹤其認證流程。
然後在idea中以debug的形式啟動程式。
DelegatingSubject
login()方法
我們發現我們進入了DelegatingSubject.login方法當中;
public class DelegatingSubject implements Subject {
//省略了其他不影響理解的程式碼
public void login(AuthenticationToken token) throws AuthenticationException {
this.clearRunAsIdentitiesInternal();
// 1. 真正做認證的還是securityManager物件
Subject subject = this.securityManager.login(this, token);
String host = null;
PrincipalCollection principals;
if (subject instanceof DelegatingSubject) {
DelegatingSubject delegating = (DelegatingSubject)subject;
principals = delegating.principals;
host = delegating.host;
} else {
principals = subject.getPrincipals();
}
if (principals != null && !principals.isEmpty()) {
this.principals = principals;
this.authenticated = true;
if (token instanceof HostAuthenticationToken) {
host = ((HostAuthenticationToken)token).getHost();
}
if (host != null) {
this.host = host;
}
Session session = subject.getSession(false);
if (session != null) {
this.session = this.decorate(session);
} else {
this.session = null;
}
} else {
String msg = "Principals returned from securityManager.login( token ) returned a null or empty value. This value must be non null and populated with one or more elements.";
throw new IllegalStateException(msg);
}
}
}
從上面的原始碼中我們發現,雖然我們呼叫了Subject物件的認證方法,但是,真正的認證操作還是由安全管理器物件securityManager執行。
DefaultSecurityManager
login() 方法
接著,我們進入到securityManager的login方法當中去。
public class DefaultSecurityManager extends SessionsSecurityManager {
//省略了其他無關程式碼
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {
//呼叫認證方法
info = this.authenticate(token);
} catch (AuthenticationException var7) {
AuthenticationException ae = var7;
try {
this.onFailedLogin(token, ae, subject);
} catch (Exception var6) {
if (log.isInfoEnabled()) {
log.info("onFailedLogin method threw an exception. Logging and propagating original AuthenticationException.", var6);
}
}
throw var7;
}
Subject loggedIn = this.createSubject(token, info, subject);
this.onSuccessfulLogin(token, info, loggedIn);
return loggedIn;
}
}
AuthenticatingSecurityManager
authenticate()方法
當我們進入到authenticate方法中時,發現他是AuthenticatingSecurityManager的方法
public abstract class AuthenticatingSecurityManager extends RealmSecurityManager {
//省略了其他無關程式碼
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
return this.authenticator.authenticate(token);
}
}
AbstractAuthenticator
authenticate()方法
接著,他又呼叫了authenticator物件的authenticate方法
public abstract class AbstractAuthenticator implements Authenticator, LogoutAware {
//省略了其他無關方法
public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
if (token == null) {
throw new IllegalArgumentException("Method argument (authentication token) cannot be null.");
} else {
log.trace("Authentication attempt received for token [{}]", token);
AuthenticationInfo info;
try {
info = this.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 var8) {
AuthenticationException ae = null;
if (var8 instanceof AuthenticationException) {
ae = (AuthenticationException)var8;
}
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, var8);
if (log.isWarnEnabled()) {
log.warn(msg, var8);
}
}
try {
this.notifyFailure(token, ae);
} catch (Throwable var7) {
if (log.isWarnEnabled()) {
String msg = "Unable to send notification for failed authentication attempt - listener error?. Please check your AuthenticationListener implementation(s). Logging sending exception and propagating original AuthenticationException instead...";
log.warn(msg, var7);
}
}
throw ae;
}
log.debug("Authentication successful for token [{}]. Returned account [{}]", token, info);
this.notifySuccess(token, info);
return info;
}
}
}
ModularRealmAuthenticator
doAuthenticate()方法
緊接著進入到了ModularRealmAuthenticator認證器物件的doAuthenticate方法
public class ModularRealmAuthenticator extends AbstractAuthenticator {
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
this.assertRealmsConfigured();
Collection<Realm> realms = this.getRealms();
return realms.size() == 1 ?
/**終於到了真正的認證邏輯*/ this.doSingleRealmAuthentication((Realm)realms.iterator().next(), authenticationToken) : this.doMultiRealmAuthentication(realms, authenticationToken);
}
}
在這一步當中,在檢驗我們的Realms物件建立後,開始進入到doSingleRealmAuthentication方法當中進行認證操作
doSingleRealmAuthentication()方法
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken 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);
} else {
//獲取認證資訊
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);
} else {
return info;
}
}
}
AuthenticatingRealm
getAuthenticationInfo()方法
在這一步當中開始根據我們傳入的令牌獲取認證資訊
public abstract class AuthenticatingRealm extends CachingRealm implements Initializable {
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 首先從快取中獲取
AuthenticationInfo info = this.getCachedAuthenticationInfo(token);
if (info == null) {
//快取中沒有,則從持久化資料中獲取
info = this.doGetAuthenticationInfo(token);
log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
if (token != null && info != null) {
this.cacheAuthenticationInfoIfPossible(token, info);
}
} else {
log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
}
if (info != null) {
this.assertCredentialsMatch(token, info);
} else {
log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);
}
return info;
}
}
SimpleAccountRealm
doGetAuthenticationInfo()方法
從持久化資料來源中獲取登入物件資訊
public class SimpleAccountRealm extends AuthorizingRealm {
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken)token;
//根據使用者名稱查詢賬戶資訊
SimpleAccount account = this.getUser(upToken.getUsername());
//如果查詢到了賬戶資訊
if (account != null) {
//開始判斷賬戶狀態
if (account.isLocked()) {
throw new LockedAccountException("Account [" + account + "] is locked.");
}
if (account.isCredentialsExpired()) {
String msg = "The credentials for account [" + account + "] are expired";
throw new ExpiredCredentialsException(msg);
}
}
return account;
}
}
在這裡,便完成了對使用者名稱的校驗。
AuthenticatingRealm
接下來,我們獲取到了賬戶資訊並返回到了AuthenticatingRealm的getAuthenticationInfo方法。
在這個方法中有如下幾行程式碼,在第二行中,呼叫assertCredentialsMatch方法開始校驗使用者憑證
if (info != null) {
this.assertCredentialsMatch(token, info);
} else {
log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);
}
assertCredentialsMatch()方法
protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
//獲取憑證匹配器物件
CredentialsMatcher cm = this.getCredentialsMatcher();
if (cm != null) {
if (!cm.doCredentialsMatch(token, info)) {
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.");
}
}
在這裡完成對使用者憑證的校驗。真正的比較邏輯則在SimpleCredentialsMatcher的equals方法中完成。裡面還會區分加密和不加密的情況,具體請檢視原始碼。
分析到這一步我們可以發現,SimpleAccountRealm繼承了AuthorizingRealm類實現doGetAuthenticationInfo方法完成賬戶資訊查詢並校驗,並將結果返回給AuthorizingRealm。AuthorizingRealm幫SimpleAccountRealm完成對使用者憑證的校驗。
那麼,如果我們需要從資料庫當中獲取賬戶資訊,應該怎麼將賬戶資訊傳給shiro進行驗證呢?這個問題留給大家思考一下,我將在下一篇文章當中為大家解答。
寫在最後
在這篇文章當中,我們通過斷點除錯,閱讀原始碼的方式弄清楚了shiro的認證流程。我們拆開他的層層封裝,發現在SimpleAccountRealm物件中的doGetAuthenticationInfo方法中完成賬戶驗證,在AuthenticatingRealm的assertCredentialsMatch完成對使用者憑證的校驗。
在下一篇文章當中,我們將學習如何使用資料庫資訊完成認證和授權。