厲害啊!第一次見到把Shiro執行流程寫的這麼清楚的,建議收藏起來慢慢看
前言
shiro是apache的一個開源框架,是一個許可權管理的框架,實現 使用者認證、使用者授權。
spring中有spring security (原名Acegi),是一個許可權框架,它和spring依賴過於緊密,沒有shiro使用簡單。
shiro不依賴於spring,shiro不僅可以實現 web應用的許可權管理,還可以實現c/s系統,分散式系統許可權管理,shiro屬於輕量框架,越來越多企業專案開始使用shiro。
Shiro執行流程學習筆記
專案中使用到了shiro
,所以對shiro
做一些比較深的瞭解。
也不知從何瞭解起,先從shiro
的執行流程開始。
執行流程
- 首先呼叫
Subject.login(token)
Security Manager
,呼叫之前必須通過SecurityUtils.setSecurityManager()
設定; SecurityManager
負責真正的身份驗證邏輯;它會委託給Authenticator
進行身份驗證;Authenticator
才是真正的身份驗證者,Shiro API
中核心的身份認證入口點,此處可以自定義插入自己的實現;Authenticator
可能會委託給相應的AuthenticationStrategy
進行多 Realm 身份驗證,預設ModularRealmAuthenticator
會呼叫AuthenticationStrategy
Authenticator
會把相應的token
傳入Realm
,從Realm
獲取身份驗證資訊,如果沒有返回 / 丟擲異常表示身份驗證失敗了。此處可以配置多個Realm
,將按照相應的順序及策略進行訪問。
繫結執行緒
這裡從看專案原始碼開始。
看第一步,Subject.login(token)
方法。
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
物件。
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
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
接下來了解一下另一個重要的概念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
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!文章都會在裡面更新,整理的資料也會放在裡面。謝謝你的觀看,覺得文章對你有幫助的話記得關注我點個贊支援一下!