在上一篇文章當中,咱們經過一個簡單的例子,簡單地認識了一下shiro。在這篇文章當中,咱們將經過閱讀源碼的方式瞭解shiro的認證流程。java
建議你們邊讀文章邊動手調試代碼,這樣效果會更好。數據庫
shiro中的異常主要分爲兩類,一類是AuthenticationException
認證異常,一類是AuthorizationException
權限異常。分別對應http響應狀態碼中的401
和403
緩存
當認證不經過時將根據具體狀況拋出AuthenticationException的子類,當鑑權不經過時將會拋出AuthorizationException的子類。安全
咱們經過檢驗shiro是否拋出異常,從而判斷登陸對象是否經過認證、是否具有相關保護資源的訪問權限。session
這也是咱們在上一節的例子中,須要捕獲相關異常的緣由。app
接下來,咱們經過閱讀源碼的方式來分析一下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認證的入口處打個斷點,以便跟蹤其認證流程。ide
而後在idea中以debug的形式啓動程序。工具
咱們發現咱們進入了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執行。
接着,咱們進入到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; } }
當咱們進入到authenticate方法中時,發現他是AuthenticatingSecurityManager的方法
public abstract class AuthenticatingSecurityManager extends RealmSecurityManager { //省略了其餘無關代碼 public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException { return this.authenticator.authenticate(token); } }
接着,他又調用了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方法
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方法當中進行認證操做
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; } } }
在這一步當中開始根據咱們傳入的令牌獲取認證信息
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; } }
從持久化數據源中獲取登陸對象信息
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的getAuthenticationInfo方法。
在這個方法中有以下幾行代碼,在第二行中,調用assertCredentialsMatch方法開始校驗用戶憑證
if (info != null) { this.assertCredentialsMatch(token, info); } else { log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token); }
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完成對用戶憑證的校驗。
在下一篇文章當中,咱們將學習如何使用數據庫信息完成認證和受權。