Shiro使用

shiro概述

shiro是一個(gè)鑒權(quán)框架,鑒(Authentication)是指用戶登錄驗(yàn)證,權(quán)(Authorization)是指用戶權(quán)限控制。
Subject:主體,其實(shí)就是用戶
Principal:標(biāo)識(shí),理解為用戶名即可
Crendential:憑證,理解為密碼
Session:服務(wù)器端保存的用戶信息,需要配合客戶端cookie或者token。建議采用token(jwt),因?yàn)閠oken更通用,而cookie只適用于PC網(wǎng)頁(yè)。通過(guò)subject.getSession(create)可以得到session。
Role 、Permission:角色和權(quán)限,一般都是RBAC(role based access control),一個(gè)subject擁有多個(gè)角色,每個(gè)角色擁有多個(gè)權(quán)限。
ShiroFilter:filter,攔截所有url請(qǐng)求;在web.xml里配置DelegatingFilterProxy,因?yàn)閣eb.xml是容器第一步加載的,而spring ContextLoadListener也要配置在web.xml里,但是呢shiro的很多配置都是要通過(guò)spring完成,這樣就形成了延遲加載和依賴倒置(這里和依賴倒置原則里的依賴倒置不是一個(gè)意思),所以就要通過(guò)DelegatingFilterProxy了。
SecurityManager:處理登錄、登出,Subject.login/logout就是委托給SecurityManager處理的,登錄前后、登出前后,登錄成功、失敗也都在這里處理,你可以繼承SecurityManager,然后加上自己的業(yè)務(wù)邏輯。這里要注意,在登錄相關(guān)的業(yè)務(wù)代碼中,如果拋異常請(qǐng)拋AuthenticationException及其子類(lèi)。
SessionManager:保存session及從request中根據(jù)sessionId獲取session,一般session保存在分布式緩存redis中。在SecurityUtils.getSubject()就會(huì)調(diào)用SessionManager中獲取session。
SessionKey:其實(shí)就是一個(gè)字符串,看你的sessionId怎么生成的,
FilterChainResolver:根據(jù)請(qǐng)求url和shiro.xml中配置的filterChainDefinition來(lái)解析出一個(gè)url被哪些shiro filter攔截,需要和FilterChainManager結(jié)合使用。我們知道servlet中所有請(qǐng)求都被會(huì)filter攔截,多個(gè)filter之間會(huì)組成filterChain,FilterChainResolver會(huì)對(duì)url解析出一個(gè)新的filterChain,這個(gè)新的filterChain在原先f(wàn)ilterChain前面添加了shiro自己的filter,這樣先執(zhí)行shiro的filter,執(zhí)行失敗就拋出異常。
Realm:這個(gè)單詞意思是領(lǐng)域,初次可能不太知道是什么意思,其實(shí)就是shiro提供驗(yàn)證信息和權(quán)限信息的地方。一般系統(tǒng)會(huì)提供多種登錄方式,如用戶名+密碼,手機(jī)號(hào)+驗(yàn)證碼和第三方集成登錄。
用戶名+密碼:這種方式在企業(yè)內(nèi)部常用,在互聯(lián)網(wǎng)領(lǐng)域沒(méi)落了,除非你是大廠。用戶名和密碼存儲(chǔ)在數(shù)據(jù)庫(kù),密碼是經(jīng)鹽(salt)加密。
手機(jī)號(hào)+驗(yàn)證碼:這個(gè)在互聯(lián)網(wǎng)領(lǐng)域很常見(jiàn),因?yàn)楝F(xiàn)在應(yīng)用太多了,普通人很難記住這么多密碼,甚至?xí)惶酌艽a走天下。驗(yàn)證碼看情況存儲(chǔ)在數(shù)據(jù)庫(kù)或緩存中都可以。
第三方集成:一般會(huì)選擇一些大廠進(jìn)行集成,因?yàn)橛脩艨隙ㄔ谶@些大廠有賬號(hào)啊,如QQ、微信、支付寶、微博等。需要和第三方做集成,一般采用oauth2授權(quán)方式,分2步走,第一步跳到微信(以微信為例)授權(quán)頁(yè)面,需要用戶手動(dòng)點(diǎn)擊同意授權(quán),然后返回一個(gè)code,后臺(tái)需要拿這個(gè)code換取openId,后面就拿openId去訪問(wèn)用戶信息了(如昵稱、頭像等基本資料,重要資料你也拿不到的)。一般還要在本系統(tǒng)里創(chuàng)建一個(gè)新用戶,關(guān)聯(lián)上openId。有些網(wǎng)站還會(huì)讓你新建用戶名關(guān)聯(lián)手機(jī)號(hào)啥的,其實(shí)這和注冊(cè)一個(gè)用戶差不多,個(gè)人比較反感,因?yàn)槲乙呀?jīng)授權(quán)微信登錄了??梢韵仍诤笈_(tái)創(chuàng)建一個(gè)默認(rèn)賬戶,至于手機(jī)號(hào)在需要交易等需要的時(shí)候再綁定,這樣可以提高用戶轉(zhuǎn)化率、留存率。不要在一開(kāi)始就嚇跑用戶。
注意上面不管采用哪種登錄方式,最后都要轉(zhuǎn)化為本系統(tǒng)上的一個(gè)用戶才行。

上面說(shuō)了這么多,回到Realm上來(lái),Realm就是提供驗(yàn)證信息和權(quán)限信息的來(lái)源,因此有AuthenticationRealm和AuthorizingRealm 2種,其實(shí)AuthorizingRealm已經(jīng)繼承了AuthenticationRealm,因此你的Realm繼承AuthorizingRealm,實(shí)現(xiàn)getAuthenticationInfo()和getAuthorizationInfo()即可。
AuthenticationStrategy:多個(gè)Realm時(shí)是全部Realm都通過(guò)還是只需要一個(gè),默認(rèn)是只需要一個(gè)通過(guò)即可,沒(méi)遇到所有Realm都需要通過(guò)的場(chǎng)景。
Authenticator:系統(tǒng)中根據(jù)不同的登錄方式會(huì)提供不同的Realm,Authenticator就是用來(lái)決定采用哪個(gè)Realm進(jìn)行鑒定。
Authorizer:用來(lái)判定用戶是否擁有權(quán)限,也是通過(guò)Realm來(lái)獲取權(quán)限信息。
Cache:用戶驗(yàn)證信息和權(quán)限信息一般放在緩存里
CacheManager:不同類(lèi)型信息放在不同的緩存里,如驗(yàn)證信息、權(quán)限信息、用戶密碼嘗試次數(shù)等都可以放在不同的cache中。
CredentialsMatcher:憑證比對(duì)器,就是用戶給的憑證和系統(tǒng)中的憑證進(jìn)行比對(duì),通過(guò)了就認(rèn)為是合法用戶,通不過(guò)就報(bào)密碼錯(cuò)誤之類(lèi)的信息。是依賴Realm提供的getAuthenticationInfo()。不同的登錄方式匹配規(guī)則也不同,如用戶名+密碼方式要匹配密碼,手機(jī)號(hào)+驗(yàn)證碼就要匹配驗(yàn)證碼。
AuthorizationAttributeSourceAdvisor:看名字就知道這是一個(gè)Spring Advisor,用于攔截shiro的幾個(gè)角色、權(quán)限注解,如RequiresRoles,RequiresPermissions。有時(shí)間寫(xiě)一篇Spring AOP(Pointcut,Advice,Advisor)文章。
MethodInvokingFactoryBean:在shiro配置中會(huì)看到這個(gè)東西,其實(shí)這個(gè)東西就是一個(gè)反射調(diào)用對(duì)象(類(lèi))方法,因?yàn)閟hiro有的地方并不符合javabean規(guī)范,一些屬性不提供setter/getter,這里就可以通過(guò)MethodInvokingFactoryBean來(lái)設(shè)置。當(dāng)然你也可以自己寫(xiě)個(gè)bean,在你的bean里調(diào)用反射方法,其實(shí)一樣的。 MethodInvokingFactoryBean只是一個(gè)工具類(lèi)。
LifecycleBeanPostProcessor:就是一個(gè)BPP,在shiro bean生成和銷(xiāo)毀的時(shí)候做初始化、銷(xiāo)毀操作。
AccessControlFilter:這里有個(gè)很重要的方法:

 public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
    }

這個(gè)方法返回true表示繼續(xù)執(zhí)行后面的filterChain,返回false就不再執(zhí)行后續(xù)filterChain。
isAccessAllowed是判斷當(dāng)前用戶是否允許通過(guò),如果當(dāng)前用戶是登錄狀態(tài)肯定返回true,如果不是登錄狀態(tài)就要執(zhí)行后面的onAccessDenied(),在這個(gè)方法里如果沒(méi)有登錄就要跳轉(zhuǎn)登錄了,shiro的FormAuthenticationFilter就是實(shí)現(xiàn)了onAccessDenied()來(lái)實(shí)現(xiàn)登錄的,一般判斷url=loginUrl并且是post請(qǐng)求就認(rèn)為是登錄請(qǐng)求。

注意
有的人會(huì)在shiro.xml里面配置DefaultAdvisorAutoProxyCreator,其實(shí)這是不必要的,這樣會(huì)造成二次代理問(wèn)題,基本在項(xiàng)目中不要手動(dòng)配置DefaultAdvisorAutoProxyCreator,以免和spring中其他AOP如事務(wù)產(chǎn)生二次代理問(wèn)題。

注意
shiroFilter依賴了securityManager, securityManager依賴了Realm,如果你的Realm因?yàn)橐@取用戶和角色導(dǎo)致Realm依賴了UserService之類(lèi)的,會(huì)造成UserService配置的事務(wù)AOP無(wú)效,具體原因和解決方法見(jiàn):http://www.itdecent.cn/p/b1209cd3686d

注意
我們?cè)谌魏蔚胤蕉寄芡ㄟ^(guò)SecurityUtils.getSubject()獲取當(dāng)前用戶對(duì)象,其實(shí)用到的就是ThreadLocal,shiro中的ThreadContext就是專門(mén)處理Subject和ThreadLocal綁定、解綁的。

注意
Subject.getPrincipal()返回是當(dāng)前使用的用戶名,getPreviousPrincipal()返回以前的用戶名,是個(gè)棧結(jié)構(gòu),可以一直取。
當(dāng)用戶通過(guò)手機(jī)號(hào)或者微信登錄系統(tǒng)時(shí),principal一開(kāi)始是手機(jī)號(hào)和微信的code,登錄成功后principal肯定要轉(zhuǎn)成系統(tǒng)里的用戶名,因?yàn)楹竺鎔etAuthorizationInfo()和其他地方都用取subject.getPrincipal()來(lái)獲取當(dāng)前用戶標(biāo)識(shí)。
Subject.runAs()、releaseRunAs()就是處理這種場(chǎng)景,一個(gè)人以另一個(gè)身份訪問(wèn)資源。

注意
手機(jī)號(hào)+驗(yàn)證碼登錄方式,如果開(kāi)啟了authenticationInfo cache,用戶獲取了驗(yàn)證碼,然后authenticationInfo被緩存了,故意驗(yàn)證失敗,然后再次請(qǐng)求驗(yàn)證碼,再次嘗試登錄,會(huì)因?yàn)閏ache中的authenticationInfo沒(méi)清空導(dǎo)致永遠(yuǎn)登錄不成功??梢越胊uthenticationInfo cache。

注意
還有一種場(chǎng)景是在線程池中獲取當(dāng)前用戶信息,如在業(yè)務(wù)中你往線程池中提交了一個(gè)異步任務(wù),在任務(wù)代碼中要訪問(wèn)當(dāng)時(shí)用戶信息,肯定不能把subject對(duì)象當(dāng)做參數(shù)傳入,我們還要通過(guò)SecurityUtils.getSubject()獲得當(dāng)時(shí)提交任務(wù)的用戶,而線程池中的線程是沒(méi)有Subject的。其實(shí)Subject提供了方法:associateWith(Runnable),但是這種有個(gè)問(wèn)題,即用戶在提交任務(wù)后立馬登出,這樣ThreadLocal中的Subject里面數(shù)據(jù)肯定就沒(méi)有了,應(yīng)該復(fù)制出來(lái)一份Subject的,目前還沒(méi)碰到這樣的問(wèn)題,待驗(yàn)證。

登錄過(guò)程中Subject變化
在登錄成功之前,代碼里可能就已經(jīng)通過(guò)SecurityUtils.getSubject()獲取到當(dāng)前線程綁定的Subject了,此時(shí)subject.authenticated=false,在登錄成功后會(huì)返回一個(gè)authenticated=true的subject,后返回的subject里包含了前面subject里內(nèi)容。

 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
        }

        Subject loggedIn = createSubject(token, info, subject);

        onSuccessfulLogin(token, info, loggedIn);

        return loggedIn;
    }

shiro工作流程

當(dāng)用戶訪問(wèn)一個(gè)url時(shí),會(huì)被shiroFilter攔截,根據(jù)filterChainResolver會(huì)匹配對(duì)應(yīng)的攔截器filter(注意只匹配第一個(gè),不會(huì)匹配最優(yōu)的),所以需要登錄的路徑放在filterChainDefinition(LinkedHashMap)最上面,匿名訪問(wèn)url放最下面。
如果沒(méi)有登錄,就會(huì)跳到登錄頁(yè)面,登錄成功后跳轉(zhuǎn)successUrl或者SavedRequest頁(yè)面。
如果登錄了就放行。
權(quán)限的話可以用注解,這樣方便但是零散,每個(gè)方法上都要寫(xiě),重復(fù)勞動(dòng),但是靈活。
如果url有規(guī)則的話,如/user/create,, order/update這種的話可以寫(xiě)個(gè)filter處理。
shiro只能處理動(dòng)作權(quán)限,即是否有訪問(wèn)url權(quán)限,但是不能處理數(shù)據(jù)權(quán)限,如一條記錄是否能看,里面的字段是否能看,這個(gè)沒(méi)啥好方法,要么硬編碼要么自己抽規(guī)則了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容