shiro是apache的一個開源框架,是一個權限管理的框架,實現 用戶認證、用戶受權。
spring中有spring security (原名Acegi),是一個權限框架,它和spring依賴過於緊密,沒有shiro使用簡單。
shiro不依賴於spring,shiro不只能夠實現 web應用的權限管理,還能夠實現c/s系統,分佈式系統權限管理,shiro屬於輕量框架,愈來愈多企業項目開始使用shiro。web
項目中使用到了shiro
,因此對shiro
作一些比較深的瞭解。面試
也不知從何瞭解起,先從shiro
的運行流程開始。spring
Subject.login(token)
進行登陸,其會自動委託給 Security Manager
,調用以前必須經過 SecurityUtils.setSecurityManager()
設置;SecurityManager
負責真正的身份驗證邏輯;它會委託給 Authenticator
進行身份驗證;Authenticator
纔是真正的身份驗證者,Shiro API
中核心的身份認證入口點,此處能夠自定義插入本身的實現;Authenticator
可能會委託給相應的 AuthenticationStrategy
進行多 Realm 身份驗證,默認 ModularRealmAuthenticator
會調用 AuthenticationStrategy
進行多 Realm 身份驗證;Authenticator
會把相應的 token
傳入 Realm
,從 Realm
獲取身份驗證信息,若是沒有返回 / 拋出異常表示身份驗證失敗了。此處能夠配置多個 Realm
,將按照相應的順序及策略進行訪問。這裏從看項目源碼開始。數據庫
看第一步,Subject.login(token)
方法。apache
UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe); Subject subject = SecurityUtils.getSubject(); subject.login(token);
出現了一個UsernamePasswordToken
對象,它在這裏會調用它的一個構造函數。設計模式
public UsernamePasswordToken(final String username, final String password, final boolean rememberMe) { this(username, password != null ? password.toCharArray() : null, rememberMe, null); }
據筆者本身瞭解,這是shiro
的一個驗證對象,只是用來存儲用戶名密碼,以及一個記住我屬性的。安全
以後會調用shiro
的一個工具類獲得一個subject
對象。app
public static Subject getSubject() { Subject subject = ThreadContext.getSubject(); if (subject == null) { subject = (new Subject.Builder()).buildSubject(); ThreadContext.bind(subject); } return subject; }
經過getSubject
方法來獲得一個Subject
對象。框架
這裏不得不提到shiro
的內置線程類ThreadContext
,經過bind
方法會將subject
對象綁定在線程上。分佈式
public static void bind(Subject subject) { if (subject != null) { put(SUBJECT_KEY, subject); } }
public static void put(Object key, Object value) { if (key == null) { throw new IllegalArgumentException("key cannot be null"); } if (value == null) { remove(key); return; } ensureResourcesInitialized(); resources.get().put(key, value); if (log.isTraceEnabled()) { String msg = "Bound value of type [" + value.getClass().getName() + "] for key [" + key + "] to thread " + "[" + Thread.currentThread().getName() + "]"; log.trace(msg); } }
且shiro
的key
都是遵循一個固定的格式。
public static final String SUBJECT_KEY = ThreadContext.class.getName() + "_SUBJECT_KEY";
通過非空判斷後會將值以KV的形式put進去。
當你想拿到subject
對象時,也能夠經過getSubject
方法獲得subject
對象。
在綁定subject對象時,也會將securityManager
對象進行一個綁定。
而綁定securityManager
對象的地方是在Subject
類的一個靜態內部類裏(可以讓我好一頓找)。
在getSubject
方法中的一句代碼調用了內部類的buildSubject
方法。
subject = (new Subject.Builder()).buildSubject();
PS:此處運用到了建造者設計模式,能夠去菜鳥教程仔細瞭解
進去觀看源碼後能夠看見。
首先調用無參構造,在無參構造裏調用有參構造函數。
public Builder() { this(SecurityUtils.getSecurityManager()); } public Builder(SecurityManager securityManager) { if (securityManager == null) { throw new NullPointerException("SecurityManager method argument cannot be null."); } this.securityManager = securityManager; this.subjectContext = newSubjectContextInstance(); if (this.subjectContext == null) { throw new IllegalStateException("Subject instance returned from 'newSubjectContextInstance' " + "cannot be null."); } this.subjectContext.setSecurityManager(securityManager); }
在此處綁定了securityManager
對象。
固然,他也對securityManager
對象的空情況進行了處理,在getSecurityManager
方法裏。
public static SecurityManager getSecurityManager() throws UnavailableSecurityManagerException { SecurityManager securityManager = ThreadContext.getSecurityManager(); if (securityManager == null) { securityManager = SecurityUtils.securityManager; } if (securityManager == null) { String msg = "No SecurityManager accessible to the calling code, either bound to the " + ThreadContext.class.getName() + " or as a vm static singleton. This is an invalid application " + "configuration."; throw new UnavailableSecurityManagerException(msg); } return securityManager; }
真正的核心就在於securityManager
這個對象。
SecurityManager
是一個接口,他繼承了步驟裏所談到的Authenticator
,Authorizer
類以及用於Session管理的SessionManager
。
public interface SecurityManager extends Authenticator, Authorizer, SessionManager { Subject login(Subject subject, AuthenticationToken authenticationToken) throws AuthenticationException; void logout(Subject subject); Subject createSubject(SubjectContext context); }
看一下它的實現。
且這些類和接口都有依次繼承的關係。
接下來了解一下另外一個重要的概念Relam
。
Realm充當了Shiro與應用安全數據間的「橋樑」或者「鏈接器」。也就是說,當與像用戶賬戶這類安全相關數據進行交互,執行認證(登陸)和受權(訪問控制)時,Shiro會從應用配置的Realm中查找不少內容。
從這個意義上講,Realm實質上是一個安全相關的DAO:它封裝了數據源的鏈接細節,並在須要時將相關數據提供給Shiro。當配置Shiro時,你必須至少指定一個Realm,用於認證和(或)受權。配置多個Realm是能夠的,可是至少須要一個。
Shiro內置了能夠鏈接大量安全數據源(又名目錄)的Realm,如LDAP、關係數據庫(JDBC)、相似INI的文本配置資源以及屬性文件 等。若是缺省的Realm不能知足需求,你還能夠插入表明自定義數據源的本身的Realm實現。
通常狀況下,都會自定義Relam
來使用。
先看一下實現。
以及自定義的一個UserRelam
。
看一下類圖。
每一個抽象類繼承後所須要實現的方法都不同。
public class UserRealm extends AuthorizingRealm
這裏繼承AuthorizingRealm
,須要實現它的兩個方法。
//給登陸用戶受權 protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals); //這個抽象方法屬於AuthorizingRealm抽象類的父類AuthenticatingRealm類 登陸認證,也是登陸的DAO操做所在的方法 protected abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;
以後再來看看這個驗證方法,在以前的步驟裏提到了,驗證用到了Authenticator
,也就是第五步。
Authenticator
會把相應的 token
傳入 Realm
,從 Realm
獲取身份驗證信息,若是沒有返回 / 拋出異常表示身份驗證失敗了。此處能夠配置多個 Realm
,將按照相應的順序及策略進行訪問。
再回到以前登陸方法上來看看。
subject.login(token)
在第一步中調用了Subject
的login
方法,找到它的最終實現DelegatingSubject
類。
裏面有調用了securityManager
的login
方法,而最終實現就在DefaultSecurityManager
這個類裏。
Subject subject = securityManager.login(this, token);
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException { AuthenticationInfo info; try { 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 }
以後就是驗證流程,這裏咱們會看到第四步,點進去會到抽象類AuthenticatingSecurityManager
。再看看它的仔細調用。
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException { return this.authenticator.authenticate(token); }
真正的調用Relam
進行驗證並不在這,而是在ModularRealmAuthenticator
。
他們之間是一個從左到右的過程。
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { assertRealmsConfigured(); Collection<Realm> realms = getRealms(); if (realms.size() == 1) { return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken); } else { return doMultiRealmAuthentication(realms, authenticationToken); } }
在這裏我們就看這個doSingleRealmAuthentication
方法。
單Relam
驗證。
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); } //在此處調用你自定義的Relam的方法來驗證。 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; }
再看看多Relam
的。
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) { AuthenticationStrategy strategy = getAuthenticationStrategy(); AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token); for (Realm realm : realms) { aggregate = strategy.beforeAttempt(realm, token, aggregate); if (realm.supports(token)) { AuthenticationInfo info = null; Throwable t = null; try { //調用自定義的Relam的方法來驗證。 info = realm.getAuthenticationInfo(token); } catch (Throwable throwable) { t = throwable; } 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; }
會發現調用的都是Relam
的getAuthenticationInfo
方法。
看到了熟悉的UserRelam
,此致,閉環了。
可是也只是瞭解了大概的流程,對每一個類的具體做用並非很瞭解,因此筆者仍是有不少地方要去學習,不,應該說我原本就是菜雞,就要學才能變帶佬。
你們看完有什麼不懂的能夠在下方留言討論,也能夠關注我私信問我,我看到後都會回答的。也歡迎你們關注個人公衆號:前程有光,立刻金九銀十跳槽面試季,整理了1000多道將近500多頁pdf文檔的Java面試題資料放在裏面,助你圓夢BAT!文章都會在裏面更新,整理的資料也會放在裏面。謝謝你的觀看,以爲文章對你有幫助的話記得關注我點個贊支持一下!