前言
shiro是apache的一個開源框架,是一個權(quán)限管理的框架析蝴,實現(xiàn) 用戶認證害捕、用戶授權(quán)。
spring中有spring security (原名Acegi)闷畸,是一個權(quán)限框架尝盼,它和spring依賴過于緊密,沒有shiro使用簡單佑菩。
shiro不依賴于spring盾沫,shiro不僅可以實現(xiàn) web應(yīng)用的權(quán)限管理谅年,還可以實現(xiàn)c/s系統(tǒng)摩桶,分布式系統(tǒng)權(quán)限管理,shiro屬于輕量框架甥绿,越來越多企業(yè)項目開始使用shiro绞幌。
Shiro運行流程學(xué)習(xí)筆記
項目中使用到了shiro
蕾哟,所以對shiro
做一些比較深的了解。
也不知從何了解起莲蜘,先從shiro
的運行流程開始渐苏。
運行流程
- 首先調(diào)用
Subject.login(token)
進行登錄,其會自動委托給Security Manager
菇夸,調(diào)用之前必須通過SecurityUtils.setSecurityManager()
設(shè)置; -
SecurityManager
負責(zé)真正的身份驗證邏輯仪吧;它會委托給Authenticator
進行身份驗證庄新; -
Authenticator
才是真正的身份驗證者,Shiro API
中核心的身份認證入口點薯鼠,此處可以自定義插入自己的實現(xiàn)择诈; -
Authenticator
可能會委托給相應(yīng)的AuthenticationStrategy
進行多 Realm 身份驗證,默認ModularRealmAuthenticator
會調(diào)用AuthenticationStrategy
進行多 Realm 身份驗證出皇; -
Authenticator
會把相應(yīng)的token
傳入Realm
羞芍,從Realm
獲取身份驗證信息,如果沒有返回 / 拋出異常表示身份驗證失敗了郊艘。此處可以配置多個Realm
荷科,將按照相應(yīng)的順序及策略進行訪問唯咬。
綁定線程
這里從看項目源碼開始。
看第一步畏浆,Subject.login(token)
方法胆胰。
UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe);
Subject subject = SecurityUtils.getSubject();
subject.login(token);
出現(xiàn)了一個UsernamePasswordToken
對象,它在這里會調(diào)用它的一個構(gòu)造函數(shù)刻获。
public UsernamePasswordToken(final String username, final String password, final boolean rememberMe) {
this(username, password != null ? password.toCharArray() : null, rememberMe, null);
}
據(jù)筆者自己了解蜀涨,這是shiro
的一個驗證對象,只是用來存儲用戶名密碼蝎毡,以及一個記住我屬性的厚柳。
之后會調(diào)用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
的內(nèi)置線程類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";
經(jīng)過非空判斷后會將值以KV的形式put進去宰闰。
當(dāng)你想拿到subject
對象時,也可以通過getSubject
方法得到subject
對象簿透。
在綁定subject對象時移袍,也會將securityManager
對象進行一個綁定。
而綁定securityManager
對象的地方是在Subject
類的一個靜態(tài)內(nèi)部類里(可讓我好一頓找)老充。
在getSubject
方法中的一句代碼調(diào)用了內(nèi)部類的buildSubject
方法葡盗。
subject = (new Subject.Builder()).buildSubject();
PS:此處運用到了建造者設(shè)計模式,可以去菜鳥教程仔細了解
進去觀看源碼后可以看見啡浊。
首先調(diào)用無參構(gòu)造觅够,在無參構(gòu)造里調(diào)用有參構(gòu)造函數(shù)。
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
對象巷嚣。
當(dāng)然喘先,他也對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);
}
看一下它的實現(xiàn)。
且這些類和接口都有依次繼承的關(guān)系嗤放。
Relam
接下來了解一下另一個重要的概念Relam
思喊。
Realm充當(dāng)了Shiro與應(yīng)用安全數(shù)據(jù)間的“橋梁”或者“連接器”。也就是說次酌,當(dāng)與像用戶帳戶這類安全相關(guān)數(shù)據(jù)進行交互恨课,執(zhí)行認證(登錄)和授權(quán)(訪問控制)時舆乔,Shiro會從應(yīng)用配置的Realm中查找很多內(nèi)容。
從這個意義上講庄呈,Realm實質(zhì)上是一個安全相關(guān)的DAO:它封裝了數(shù)據(jù)源的連接細節(jié)蜕煌,并在需要時將相關(guān)數(shù)據(jù)提供給Shiro。當(dāng)配置Shiro時诬留,你必須至少指定一個Realm斜纪,用于認證和(或)授權(quán)。配置多個Realm是可以的文兑,但是至少需要一個盒刚。
Shiro內(nèi)置了可以連接大量安全數(shù)據(jù)源(又名目錄)的Realm,如LDAP绿贞、關(guān)系數(shù)據(jù)庫(JDBC)因块、類似INI的文本配置資源以及屬性文件 等。如果缺省的Realm不能滿足需求籍铁,你還可以插入代表自定義數(shù)據(jù)源的自己的Realm實現(xiàn)涡上。
一般情況下,都會自定義Relam
來使用拒名。
先看一下實現(xiàn)吩愧。
以及自定義的一個UserRelam
。
看一下類圖增显。
每個抽象類繼承后所需要實現(xiàn)的方法都不一樣雁佳。
public class UserRealm extends AuthorizingRealm
這里繼承AuthorizingRealm
,需要實現(xiàn)它的兩個方法同云。
//給登錄用戶授權(quán)
protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals);
//這個抽象方法屬于AuthorizingRealm抽象類的父類AuthenticatingRealm類 登錄認證糖权,也是登錄的DAO操作所在的方法
protected abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;
之后再來看看這個驗證方法,在之前的步驟里提到了炸站,驗證用到了Authenticator
星澳,也就是第五步。
Authenticator
Authenticator
會把相應(yīng)的 token
傳入 Realm
旱易,從 Realm
獲取身份驗證信息募判,如果沒有返回 / 拋出異常表示身份驗證失敗了。此處可以配置多個 Realm
咒唆,將按照相應(yīng)的順序及策略進行訪問。
再回到之前登錄方法上來看看释液。
subject.login(token)
在第一步中調(diào)用了Subject
的login
方法全释,找到它的最終實現(xiàn)DelegatingSubject
類。
里面有調(diào)用了securityManager
的login
方法误债,而最終實現(xiàn)就在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
李命。再看看它的仔細調(diào)用登淘。
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
return this.authenticator.authenticate(token);
}
真正的調(diào)用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);
}
//在此處調(diào)用你自定義的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 {
//調(diào)用自定義的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;
}
會發(fā)現(xiàn)調(diào)用的都是Relam
的getAuthenticationInfo
方法绅这。
看到了熟悉的UserRelam
,此致在辆,閉環(huán)了证薇。
但是也只是了解了大概的流程,對每個類的具體作用并不是很了解匆篓,所以筆者還是有很多地方要去學(xué)習(xí)浑度,不,應(yīng)該說我本來就是菜雞奕删,就要學(xué)才能變帶佬俺泣。
最后
感謝你看到這里,看完有什么的不懂的可以在評論區(qū)問我完残,覺得文章對你有幫助的話記得給我點個贊伏钠,每天都會分享java相關(guān)技術(shù)文章或行業(yè)資訊,歡迎大家關(guān)注和轉(zhuǎn)發(fā)文章谨设!