在Shiro中,登陸操做是由Subject的login()
方法完成的,Subject
是個接口,在Web環境中,實現類爲WebDelegatingSubject
,login
方法從DeletatingSubject
繼承而來:java
public void login(AuthenticationToken token) throws AuthenticationException { clearRunAsIdentitiesInternal(); Subject subject = securityManager.login(this, token); // 省略一些代碼... }
由上可見,Subject.login()
方法委託給了SecurityManager
對象,在Web環境中,SecurityManager
實現類爲DefaultWebSecurityManager
,其login
方法從DefaultSecurityManager
繼承而來:數據庫
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException { AuthenticationInfo info; try { // 對提交的AuthenticationToken進行認證 info = authenticate(token); } catch (AuthenticationException ae) { try { // 若是認證失敗 onFailedLogin(token, ae, subject); } catch (Exception e) { if (log.isInfoEnabled()) { log.info("onFailedLogin method threw an " + "exception. Logging and propagating original AuthenticationException.", e); } } throw ae; //propagate,若是認證失敗,使異常繼續向上傳播,從而返回至登陸頁面(見上篇) } // 若是認證成功則從新建立Subject對象 Subject loggedIn = createSubject(token, info, subject); // 登陸成功,主要處理RememberMe操做,即將登陸信息存儲在cookie中 onSuccessfulLogin(token, info, loggedIn); return loggedIn; }
最關鍵的authenticate
方法:設計模式
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException { return this.authenticator.authenticate(token); }
SecurityManager
把認證方法委託給認證器Authenticator
的authenticate
方法,Authenticator
的實現類爲:ModularRealmAuthenticator
,其能夠實現多認證信息源綜合認證。ModularRealmAuthenticator
實現使用了模版方法設計模式,隨後執行doAuthenticate
方法:緩存
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { assertRealmsConfigured(); // Realm集合在爲SecurityManager設置Realm時就會設置給Authenticator // 至於Realm表明什麼,請參看:http://jinnianshilongnian.iteye.com/blog/2018936 Collection<Realm> realms = getRealms(); if (realms.size() == 1) { // 若是Realm只有一個 return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken); } else { // 若是Realm有多個 return doMultiRealmAuthentication(realms, authenticationToken); } }
單一Realm認證:cookie
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) { // 該Realm是否支持此種Token,由於並非任何一種Realm與AuthenticationToken都是相互匹配的 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); } // 根據AuthenticationToken獲取認證信息 // Realm通常是由本身實現的,雖說Shiro有一些本身的實現,可是在實際項目中,Shiro的實現直接就能使用的狀況不多 // 比較將認證信息(用戶名密碼等)存在數據庫,則該getAuthenticationInfo方法就是根據Token中的信息去數據庫中查找、 // 匹配,若是匹配上了則返回相應認證後的認證信息 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會繼承自AuthenticatingRealm
,因此會執行至:app
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) { // 斷言AuthenticationToken與AuthenticationInfo是匹配的,簡單點來講就是判斷密碼是否正確,不正確則拋異常 // doGetAuthenticationInfo方法主要判斷帳戶是否存在 assertCredentialsMatch(token, info); } else { log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token); } return info; }
單一Realm認證時,只須要判斷一個Realm認證是否成功便可,可是當存在多個Realm時狀況就有點複雜了。由於有可能有些Realm認證成功了,有些Realm又認證失敗了,這時到底算是認證成功仍是失敗呢?因此這時Shiro使用了策略模式,用具體的策略類來處理這個問題。多個Realm認證時的doMultiRealmAuthentication
方法以下:ide
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) { // 首先就得獲取認證策略,Shiro實現了三種: //1. AllSuccessfulStrategy: 必須全部Realm認證成功了纔算是認證成功 //2. AtLeastOneSuccessfulStrategy: 至少有一個Realm認證成功了就算是認證成功 //3. FirstSuccessfulStrategy: 第一個Realm認證成功了就算是認證成功 // 默認實現爲AtLeastOneSuccessfulStrategy AuthenticationStrategy strategy = getAuthenticationStrategy(); // 假設咱們如今使用的就是AtLeastOneSuccessfulStrategy // 返回SimpleAuthenticationInfo,這是一個空認證信息,並不含有principal與credentials AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token); if (log.isTraceEnabled()) { log.trace("Iterating through {} realms for PAM authentication", realms.size()); } for (Realm realm : realms) { // 直接返回aggregate 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); } } //若是認證成功則info不爲null,且包含有principal與credentials //afterAttempt方法會將info與aggregate合併,也就是將AuthenticationInfo的principal與credentials //分別用一集合存儲 aggregate = strategy.afterAttempt(realm, token, info, aggregate, t); } else { log.debug("Realm [{}] does not support token {}. Skipping realm.", realm, token); } } // 檢測合併後的AuthenticationInfo中是否含用principal,若是有則返回aggregate // 沒有則拋出異常認證失敗,因而可知只要有一個Realm認證成功則算是認證成功 aggregate = strategy.afterAllAttempts(token, aggregate); return aggregate; }
上面只分析了AtLeastOneSuccessfulStrategy
策略,其它兩個請自行查看源碼。this
假設如今認證成功了,接下來執行DefaultSecurityManager.createSubject
方法:debug
protected Subject createSubject(AuthenticationToken token, AuthenticationInfo info, Subject existing) { // 建立SubjectContext對象 SubjectContext context = createSubjectContext(); // 設置爲已認證 context.setAuthenticated(true); // 設置Token context.setAuthenticationToken(token); // 設置認證經過後的認證信息 context.setAuthenticationInfo(info); if (existing != null) { // 設置先前存在的Subject context.setSubject(existing); } return createSubject(context); } public Subject createSubject(SubjectContext subjectContext) { // 複製SubjectContext,原SubjectContext信息得以保留 SubjectContext context = copy(subjectContext); // 確保SubjectContext與SecurityManager關聯 context = ensureSecurityManager(context); // 解析會話,有可能使用Servlet中的Session實現,也可能使用Shiro本身實現的Session context = resolveSession(context); context = resolvePrincipals(context); // 交由DefaultWebSubjectFactory.createSubject從新建立Subject Subject subject = doCreateSubject(context); // 將Subject中的principal與credentials存儲在Session中 save(subject); return subject; }
-------------------------------- END -------------------------------設計
及時獲取更多精彩文章,請關注公衆號《Java精講》。