Shiro 登录认证源码详解
Apache Shiro 是一個(gè)強(qiáng)大且靈活的 Java 開源安全框架,擁有登錄認(rèn)證、授權(quán)管理、企業(yè)級會話管理和加密等功能,相比 Spring Security 來說要更加的簡單。
本文主要介紹 Shiro 的登錄認(rèn)證(Authentication)功能,主要從 Shiro 設(shè)計(jì)的角度去看這個(gè)登錄認(rèn)證的過程。
一、Shiro 總覽
首先,我們思考整個(gè)認(rèn)證過程的業(yè)務(wù)邏輯:
我們現(xiàn)在來看看 Shiro 是如何設(shè)計(jì)這個(gè)過程的:
圖中包含三個(gè)重要的 Shiro 概念:Subject、SecurityManager、Realm。接下來,分別介紹這三者有何用:
- Subject:表示“用戶”,表示當(dāng)前執(zhí)行的用戶。Subject 實(shí)例全部都綁定到了一個(gè) SecurityManager 上,當(dāng)和 Subject 交互時(shí),它是委托給 SecurityManager 去執(zhí)行的。
- SecurityManager:Shiro 結(jié)構(gòu)的心臟,協(xié)調(diào)它內(nèi)部的安全組件(如登錄,授權(quán),數(shù)據(jù)源等)。當(dāng)整個(gè)應(yīng)用配置好了以后,大多數(shù)時(shí)候都是直接和 Subject 的 API 打交道。
- Realm:數(shù)據(jù)源,也就是抽象意義上的 DAO 層。它負(fù)責(zé)和安全數(shù)據(jù)交互(比如存儲在數(shù)據(jù)庫的賬號、密碼,權(quán)限等信息),包括獲取和驗(yàn)證。Shiro 支持多個(gè) Realm,但是至少也要有一個(gè)。Shiro 自帶了很多開箱即用的 Reams,比如支持 LDAP、關(guān)系數(shù)據(jù)庫(JDBC)、INI 和 properties 文件等。但是很多時(shí)候我們都需要實(shí)現(xiàn)自己的 Ream 去完成獲取數(shù)據(jù)和判斷的功能。
登錄驗(yàn)證的過程就是:Subject 執(zhí)行 login 方法,傳入登錄的「用戶名」和「密碼」,然后 SecurityManager 將這個(gè) login 操作委托給內(nèi)部的登錄模塊,登錄模塊就調(diào)用 Realm 去獲取安全的「用戶名」和「密碼」,然后對比,一致則登錄,不一致則登錄失敗。
Shiro 詳細(xì)結(jié)構(gòu):
二、Shiro 登錄示例
代碼來自 Shiro 官網(wǎng)教程。Shiro 配置 INI 文件:
# ---------------------------------------------------------------------------- # Users and their (optional) assigned roles # username = password, role1, role2, ..., roleN # ---------------------------------------------------------------------------- [users] wang=123測試 main 方法:
public static void main(String[] args) {log.info("My First Apache Shiro Application");//1.從 Ini 配置文件中獲取 SecurityManager 工廠Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");//2.獲取 SecurityManager 實(shí)例SecurityManager securityManager = factory.getInstance();//3.將 SecurityManager 實(shí)例綁定給 SecurityUtilsSecurityUtils.setSecurityManager(securityManager);//4.獲取當(dāng)前登錄用戶Subject currentUser = SecurityUtils.getSubject();//5.判斷是否登錄,如果未登錄,則登錄if (!currentUser.isAuthenticated()) {//6.創(chuàng)建用戶名/密碼驗(yàn)證Token(Web 應(yīng)用中即為前臺獲取的用戶名/密碼)UsernamePasswordToken token = new UsernamePasswordToken("wang", "123");try {//7.執(zhí)行登錄,如果登錄未成功,則捕獲相應(yīng)的異常currentUser.login(token);} catch (UnknownAccountException uae) {log.info("There is no user with username of " + token.getPrincipal());} catch (IncorrectCredentialsException ice) {log.info("Password for account " + token.getPrincipal() + " was incorrect!");} catch (LockedAccountException lae) {log.info("The account for username " + token.getPrincipal() + " is locked. " +"Please contact your administrator to unlock it.");}// ... catch more exceptions here (maybe custom ones specific to your application?catch (AuthenticationException ae) {//unexpected condition? error?}}}三、登錄邏輯詳解
Shiro 登錄過程主要涉及到 Subject.login 方法,接下來我們將通過查看源碼來分析整個(gè)登錄過程。
3.1 創(chuàng)建AuthenticationToken
第一步是創(chuàng)建 AuthenticationToken 接口的身份 token,比如例子中的 UsernamePasswordToken。
package org.apache.shiro.authc;public interface AuthenticationToken extends Serializable {// 獲取“用戶名”Object getPrincipal();// 獲取“密碼”Object getCredentials(); }3.2 獲取當(dāng)前用戶并執(zhí)行登錄
獲取的 Subject 當(dāng)前用戶是我們平時(shí)打交道最多的接口,有很多方法,但是這里我們只分析 login 方法。
package org.apache.shiro.subject;public interface Subject {void login(AuthenticationToken token) throws AuthenticationException;}login 方法接受一個(gè) AuthenticationToken 參數(shù),如果登錄失敗則拋出 AuthenticationException 異常,可通過判斷異常類型來知悉具體的錯(cuò)誤類型。
接下來,分析 Subject 接口的實(shí)現(xiàn)類 DelegatingSubject 是如何實(shí)現(xiàn) login 方法的:
public void login(AuthenticationToken token) throws AuthenticationException {clearRunAsIdentitiesInternal();// 代理給SecurityManagerSubject subject = securityManager.login(this, token);... }3.3 SecurityManager 接口
前面說過,整個(gè) Shiro 安全框架的心臟就是 SecurityManager,我們看這個(gè)接口都有哪些方法:
package org.apache.shiro.mgt;public interface SecurityManager extends Authenticator, Authorizer, SessionManager {Subject login(Subject subject, AuthenticationToken authenticationToken) throws AuthenticationException;void logout(Subject subject);Subject createSubject(SubjectContext context); }SecurityManager 包含很多內(nèi)置的模塊來完成功能,比如登錄(Authenticator),權(quán)限驗(yàn)證(Authorizer)等。這里我們看到 SecurityManager 接口繼承了 Authenticator 登錄認(rèn)證的接口:
package org.apache.shiro.authc;public interface Authenticator {public AuthenticationInfo authenticate(AuthenticationToken authenticationToken)throws AuthenticationException; }那么,SecurityManager 的實(shí)現(xiàn)都是怎樣來實(shí)現(xiàn) Authenticator 接口的呢?答案是:使用了組合。SecurityManager 都擁有一個(gè) Authenticator 的屬性,這樣調(diào)用 SecurityManager.authenticate 的時(shí)候,是委托給內(nèi)部的 Authenticator 屬性去執(zhí)行的。
3.4 SecurityManager.login 的實(shí)現(xiàn)
// DefaultSecurityManager.java 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; }// AuthenticatingSecurityManager.java /*** Delegates to the wrapped {@link org.apache.shiro.authc.Authenticator Authenticator} for authentication.*/ public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {return this.authenticator.authenticate(token); }3.5 Authenticator 登錄模塊
Authenticator 接口如下:
package org.apache.shiro.authc;public interface Authenticator {public AuthenticationInfo authenticate(AuthenticationToken authenticationToken)throws AuthenticationException; }其實(shí)現(xiàn)類有 AbstractAuthenticator 和 ModularRealmAuthenticator:
下面來看看如何實(shí)現(xiàn)的 authenticate 方法:
// AbstractAuthenticator.java public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {AuthenticationInfo info;try {// 調(diào)用doAuthenticate方法info = doAuthenticate(token);if (info == null) {...}} catch (Throwable t) {...}... }// ModularRealmAuthenticator.java protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {assertRealmsConfigured();Collection<Realm> realms = getRealms();if (realms.size() == 1) {// Realm唯一時(shí)return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);} else {return doMultiRealmAuthentication(realms, authenticationToken);} }protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {if (!realm.supports(token)) {...}// 調(diào)用Realm的getAuthenticationInfo方法獲取AuthenticationInfo信息AuthenticationInfo info = realm.getAuthenticationInfo(token);if (info == null) {...}return info; }從源碼中可以看出,最后會調(diào)用 Realm 的 getAuthenticationInfo(AuthenticationToken) 方法。
3.6 Realm 接口
Realm 相當(dāng)于數(shù)據(jù)源,功能是通過 AuthenticationToken 獲取數(shù)據(jù)源中的安全數(shù)據(jù),這個(gè)過程中可以拋出異常,告訴 shiro 登錄失敗。
package org.apache.shiro.realm;public interface Realm {// 獲取 shiro 唯一的 realm 名稱String getName();// 是否支持給定的 AuthenticationToken 類型boolean supports(AuthenticationToken token);// 獲取 AuthenticationInfoAuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException; }Shiro 自帶了很多開箱即用的 Realm 實(shí)現(xiàn),具體的類圖如下:
3.7 總結(jié)
到此,我們把整個(gè) Shiro 的登錄認(rèn)證流程分析了一遍。
整個(gè)過程中,如果登錄失敗,就拋出異常,是使用異常來進(jìn)行邏輯控制的。
四、登錄密碼的存儲
五、學(xué)習(xí) Shiro 源碼感悟
六、參考
總結(jié)
以上是生活随笔為你收集整理的Shiro 登录认证源码详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 剑指offer全套题解:Python版
- 下一篇: 机器学习实践:TensorFlow2 多