您好,登錄后才能下訂單哦!
本篇內(nèi)容介紹了“Shiro認(rèn)證與授權(quán)原理是什么”的有關(guān)知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!
Shiro作為常用的權(quán)限框架,可被用于解決認(rèn)證、授權(quán)、加密、會話管理等場景。Shiro對其API進(jìn)行了友好的封裝,如果單純的使用Shiro框架非常簡單。但如果使用了多年Shiro,還依舊停留在基本的使用上,那么這篇文章就值得你學(xué)習(xí)一下。只有了解Shiro的底層和實現(xiàn),才能夠更好的使用和借鑒,同時也能夠避免不必要的坑。
下面以官方提供的實例為基礎(chǔ),講解分析Shiro的基本使用流程,同時針對認(rèn)證和授權(quán)流程進(jìn)行更底層的原理講解,讓大家真正了解我們所使用的Shiro框架,底層是怎么運作的。
在學(xué)習(xí)Shiro各個功能模塊之前,需要先從整體上了解Shiro的整體架構(gòu),以及核心組件所處的位置。下面為官方提供的架構(gòu)圖:
上圖可以看出Security Manager是Shiro的核心,無論認(rèn)證、授權(quán)、會話管理等都是通過它來進(jìn)行管理的。在使用和分析原理之前,先來了解后面會用到的組件及其功能:
Subject:主體,可以是用戶或程序,主體可以訪問Security Manager以獲得認(rèn)證、授權(quán)、會話等服務(wù);
Security Manager:安全管理器,主體所需的認(rèn)證、授權(quán)功能都是在這里進(jìn)行的,是Shiro的核心;
Authenticator:認(rèn)證器,主體的認(rèn)證過程通過Authenticator進(jìn)行;
Authorizer:授權(quán)器,主體的授權(quán)過程通過Authorizer進(jìn)行;
Session Manager:shiro的會話管理器,與web應(yīng)用提供的Session管理分隔開;
Realm:域,可以有一個或多個域,可通過Realm存儲授權(quán)和認(rèn)證的邏輯;
上面只列出了部分組件及功能,其他更多組件在后續(xù)文章會逐步為大家實踐講解。了解了這些組件和核心功能之后,下面以官方的示例進(jìn)行講解。
Shiro官方示例地址為:http://shiro.apache.org/tutorial.html ,需要留意的是官方示例已經(jīng)有些老了,在實踐中會做一些調(diào)整。
我們先在本地將環(huán)境搭建起來,運行程序。創(chuàng)建一個基于Maven的Java項目,引入如下依賴:
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.7.0</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <version>1.7.29</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.29</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency>
目前最新版本為1.7.0,可根據(jù)需要引入其他版本。運行官方實例比較坑的地方是,還需要引入log4j和slf4j對應(yīng)的依賴。都是Apache的項目,因此底層默認(rèn)采用了log4j的日志框架,如果不引入對應(yīng)的日志依賴,會報錯或無法打印日志。
緊接著,在resources目錄下創(chuàng)建一個shiro.ini文件,將官網(wǎng)提供內(nèi)容配置內(nèi)容復(fù)制進(jìn)去:
# ============================================================================= # Tutorial INI configuration # # Usernames/passwords are based on the classic Mel Brooks' film "Spaceballs" :) # ============================================================================= # ----------------------------------------------------------------------------- # Users and their (optional) assigned roles # username = password, role1, role2, ..., roleN # ----------------------------------------------------------------------------- [users] root = secret, admin guest = guest, guest presidentskroob = 12345, president darkhelmet = ludicrousspeed, darklord, schwartz lonestarr = vespa, goodguy, schwartz # ----------------------------------------------------------------------------- # Roles with assigned permissions # roleName = perm1, perm2, ..., permN # ----------------------------------------------------------------------------- [roles] admin = * schwartz = lightsaber:* goodguy = winnebago:drive:eagle5
這個文件可看成是一個Realm,其實就是shiro默認(rèn)的IniRealm,當(dāng)然在不同的項目中用戶、權(quán)限、角色等信息可以以各種形式存儲,比如數(shù)據(jù)庫存儲、緩存存儲等。
上述配置文件格式的語義也比較明確,配置了用戶和角色等信息,大家留意看一下注釋中對數(shù)據(jù)格式的解釋。root = secret, admin表示用戶名root,密碼是secret,角色是admin。其中角色可以配置多個,在后面依次用逗號分隔即可。schwartz = lightsaber:*表示角色schwartz擁有權(quán)限lightsaber:*。
繼續(xù)創(chuàng)建一個Tutorial類,將官網(wǎng)提供的代碼復(fù)制進(jìn)去,由于采用的是1.7.0版本,官網(wǎng)實例中下面的代碼已經(jīng)沒辦法正常運行了:
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); SecurityManager securityManager = factory.getInstance(); SecurityUtils.setSecurityManager(securityManager);
原因是IniSecurityManagerFactory類已經(jīng)被標(biāo)注廢棄了,替代它的是Environment接口及其實現(xiàn)類。因此需將上述獲取SecurityManager的方式改為通過shiro提供的Environment來初始化和獲取:
Environment environment = new BasicIniEnvironment("classpath:shiro.ini"); SecurityManager securityManager = environment.getSecurityManager(); SecurityUtils.setSecurityManager(securityManager);
改造之后的完整代碼如下(其中英文注釋已翻譯成中文注釋):
public class Tutorial { private static final transient Logger log = LoggerFactory.getLogger(Tutorial.class); public static void main(String[] args) { log.info("My First Apache Shiro Application"); // 1.初始化環(huán)境,主要是加載shiro.ini配置文件的信息 Environment environment = new BasicIniEnvironment("classpath:shiro.ini"); // 2.獲取SecurityManager安全管理器 SecurityManager securityManager = environment.getSecurityManager(); SecurityUtils.setSecurityManager(securityManager); // 3.獲取當(dāng)前主體(用戶) Subject currentUser = SecurityUtils.getSubject(); // 4.獲取當(dāng)前主體的會話 Session session = currentUser.getSession(); // 5.向會話中存儲一些內(nèi)容(不需要web容器或EJB容器) session.setAttribute("someKey", "aValue"); // 6.再次從會話中獲取存儲的內(nèi)容,并比較與存儲值是否一致。 String value = (String) session.getAttribute("someKey"); if ("aValue".equals(value)) { log.info("Retrieved the correct value! [" + value + "]"); } // 當(dāng)前用戶進(jìn)行登錄操作,進(jìn)而可以檢驗用戶的角色和權(quán)限。 // 7.判斷當(dāng)前用戶是否認(rèn)證(此時很顯然未認(rèn)證) if (!currentUser.isAuthenticated()) { // 8.將賬號和密碼封裝到UsernamePasswordToken中 UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa"); // 9.記住我 token.setRememberMe(true); try { // 10.進(jìn)行登錄操作 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."); } // ... 更多其他異常,包括應(yīng)用程序異常 catch (AuthenticationException ae) { // 其他意外異常、error處理 } } // 打印當(dāng)前用戶的主體信息 log.info("User [" + currentUser.getPrincipal() + "] logged in successfully."); // 10.檢查是否有指定角色權(quán)限(前面已經(jīng)通過Environment加載了權(quán)限和角色信息) if (currentUser.hasRole("schwartz")) { log.info("May the Schwartz be with you!"); } else { log.info("Hello, mere mortal."); } // 判斷是否有資源操作權(quán)限 if (currentUser.isPermitted("lightsaber:wield")) { log.info("You may use a lightsaber ring. Use it wisely."); } else { log.info("Sorry, lightsaber rings are for schwartz masters only."); } // 更強(qiáng)級別的權(quán)限驗證 if (currentUser.isPermitted("winnebago:drive:eagle5")) { log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. " + "Here are the keys - have fun!"); } else { log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!"); } // 11.登出 currentUser.logout(); System.exit(0); } }
完整項目源碼:https://github.com/secbr/shiro
執(zhí)行程序,打印日志信息如下,可以看到每一步的執(zhí)行輸出:
INFO - My First Apache Shiro Application INFO - Enabling session validation scheduler... INFO - Retrieved the correct value! [aValue] INFO - User [lonestarr] logged in successfully. INFO - May the Schwartz be with you! INFO - You may use a lightsaber ring. Use it wisely. INFO - You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. Here are the keys - have fun!
上述代碼中包含了11個主要的流程:
1、初始化環(huán)境,這里主要是加載shiro.ini配置文件的信息;
2、獲取SecurityManager安全管理器;
3、獲取當(dāng)前主體(用戶);
4、獲取當(dāng)前主體的會話;
5、向會話中存儲一些內(nèi)容(不需要web容器或EJB容器);
6、再次從會話中獲取存儲的內(nèi)容,并比較與存儲值是否一致;
7、判斷當(dāng)前用戶是否認(rèn)證;
8、將賬號和密碼封裝到UsernamePasswordToken中;
9、開啟記住我;
10、檢查是否有指定角色權(quán)限;
11、退出登錄。
下面我們對幾個核心步驟步驟進(jìn)行分析說明。
源碼中通過Environment對象來加載配置文件和初始化SecurityManager,然后通過工具類SecurityUtils對SecurityManager進(jìn)行設(shè)置。在實踐中,可根據(jù)具體情況進(jìn)行初始化,比如實例中通過Environment加載文件,也可以直接創(chuàng)建DefaultSecurityManager,在web項目采用DefaultWebSecurityManager等。
// 1.初始化環(huán)境,主要是加載shiro.ini配置文件的信息 Environment environment = new BasicIniEnvironment("classpath:shiro.ini"); // 2.獲取SecurityManager安全管理器 SecurityManager securityManager = environment.getSecurityManager(); SecurityUtils.setSecurityManager(securityManager);
這里的配置文件相當(dāng)于一個Realm,部分SecurityManager實現(xiàn)類(比如:DefaultSecurityManager)提供了setRealm方法,用戶可通過該方法自定義設(shè)置Realm。
總之,無論獲取SecurityManager的方式如何,都需要有這么一個SecurityManager用來處理后續(xù)的認(rèn)證、授權(quán)等處理,可見SecurityManager的核心地位。
在上述實例代碼中,先將認(rèn)證功能相關(guān)的核心代碼抽離出來,包含以下代碼及操作步驟(省略了SecurityManager的創(chuàng)建和設(shè)置):
// 獲取當(dāng)前主體(用戶) Subject currentUser = SecurityUtils.getSubject(); // 判斷當(dāng)前用戶是否認(rèn)證 currentUser.isAuthenticated() // 將賬號和密碼封裝到UsernamePasswordToken中 UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa"); // 記住我 token.setRememberMe(true); // 登錄操作 currentUser.login(token);
從這個代碼流程上來看,Shiro的認(rèn)證過程包括:初始化環(huán)境,獲取當(dāng)前用戶主體,判斷是否認(rèn)證過,將賬號密碼進(jìn)行封裝,進(jìn)行認(rèn)證,認(rèn)證完成校驗權(quán)限??梢酝ㄟ^下圖來表示整個流程。
接下來,通過跟蹤源碼,來看看Shiro的認(rèn)證流程涉及到哪些組件。
認(rèn)證的入口程序是login方法,以此方法為入口,進(jìn)行跟蹤,并忽略掉非核心操作,可得出認(rèn)證邏輯經(jīng)過以下代碼執(zhí)行步驟:
//currentUser類型為Subject,在構(gòu)造了SecurityManager之后,提交認(rèn)證,token封裝了用戶信息 currentUser.login(token); //DelegatingSubject類中,調(diào)用SecurityManager執(zhí)行認(rèn)證 Subject subject = this.securityManager.login(this, token); //DefaultSecurityManager類中,SecurityManager委托給Authenticator執(zhí)行認(rèn)證邏輯 AuthenticationInfo info = this.authenticate(token); // AuthenticatingSecurityManager類中,進(jìn)行認(rèn)證 this.authenticator.authenticate(token); //AbstractAuthenticator類中,進(jìn)行認(rèn)證 AuthenticationInfo info = this.doAuthenticate(token); //ModularRealmAuthenticator類中,獲取多Realm進(jìn)行身份認(rèn)證 Collection<Realm> realms = this.getRealms(); doSingleRealmAuthentication(realm, token); //ModularRealmAuthenticator類中,針對具體的Realm進(jìn)行身份認(rèn)證 AuthenticationInfo info = realm.getAuthenticationInfo(token); //AuthenticatingRealm類中,調(diào)用對應(yīng)的Realm進(jìn)行校驗,認(rèn)證成功則返回用戶屬性 AuthenticationInfo info = realm.doGetAuthenticationInfo(token); //SimpleAccountRealm類中,根據(jù)token獲取賬戶信息 UsernamePasswordToken upToken = (UsernamePasswordToken) token; SimpleAccount account = getUser(upToken.getUsername()); //AuthenticatingRealm類中,比對傳入的token和根據(jù)token獲取到的賬戶信息 assertCredentialsMatch(token, info); ->getCredentialsMatcher().doCredentialsMatch(token, info); //SimpleCredentialsMatcher類中,進(jìn)行具體對比 byte[] tokenBytes = toBytes(tokenCredentials); byte[] accountBytes = toBytes(accountCredentials); MessageDigest.isEqual(tokenBytes, accountBytes); //或 accountCredentials.equals(tokenCredentials);
上述代碼包括了認(rèn)證過程中一些核心流程,抽離出核心部分,整理成流程圖如下:
可以看出,整個認(rèn)證過程中涉及到了SecurityManager、Subject、Authenticator、Realm等組件,相關(guān)組件的功能可參考架構(gòu)圖中的功能說明。
實例中授權(quán)調(diào)用的代碼比較少,主要就是以下幾個方法:
// 檢查是否有相應(yīng)角色權(quán)限 currentUser.hasRole("schwartz") // 判斷是否有資源操作權(quán)限 currentUser.isPermitted("lightsaber:wield") // 判斷是否有(更細(xì)粒度的)資源操作權(quán)限 currentUser.isPermitted("winnebago:drive:eagle5")
下面以hasRole方法為例,進(jìn)行追蹤分析源代碼,看看具體的實現(xiàn)原理。
// 檢查是否有相應(yīng)角色權(quán)限 currentUser.hasRole("schwartz"); // DelegatingSubject類中,委托給SecurityManager判斷角色與既定角色是否匹配 this.securityManager.hasRole(this.getPrincipals(), roleIdentifier); // AuthorizingSecurityManager類中,SecurityManager委托Authorizer進(jìn)行角色檢驗 this.authorizer.hasRole(principals, roleIdentifier); // ModularRealmAuthorizer類中,獲取所有Realm,并遍歷檢查角色 for (Realm realm : getRealms()); ((Authorizer) realm).hasRole(principals, roleIdentifier) // AuthorizingRealm中,Authorizer判斷Realm中的角色/權(quán)限是否和傳入的匹配 AuthorizationInfo info = getAuthorizationInfo(principal); // AuthorizingRealm中,執(zhí)行Realm進(jìn)行授權(quán)操作 AuthorizationInfo info = this.doGetAuthorizationInfo(principals); // SimpleAccountRealm類中,獲得用戶SimpleAccount(實現(xiàn)了AuthorizationInfo), // users類型為Map,以用戶名為key,對應(yīng)shiro.ini中配置的初始化用戶信息 return this.users.get(username); // AuthorizingRealm類中,判斷傳入的用戶和初始化配置的是否匹配 return hasRole(roleIdentifier, info); // AuthorizingRealm類中,最終的授權(quán)判斷 return info != null && info.getRoles() != null && info.getRoles().contains(roleIdentifier);
上述代碼包括了授權(quán)過程中一些核心流程,抽離出核心部分,整理成流程圖(isPermitted方法類似,讀者可自行追蹤),如下:
可以看出,整個認(rèn)證過程中涉及到了SecurityManager、Subject、Authorizer、Realm等組件,相關(guān)組件的功能可參考架構(gòu)圖中的功能說明。
通過上面認(rèn)證和授權(quán)流程及原理的分析,會發(fā)現(xiàn)無論哪個操作都需要通過Realm來定義用戶認(rèn)證時需要的賬戶信息和授權(quán)時的權(quán)限信息。但一般情況下不會使用官網(wǎng)示例的基于“ini配置文件”的方式,而是通過自定義Realm組件來實現(xiàn)。
以下面的示例來說,我們可以使用Shiro內(nèi)置的Realm組件:
public class AuthenticationTest { SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm(); @Before public void addUser() { // 在方法開始前添加一個用戶 simpleAccountRealm.addAccount("wmyskxz", "123456"); } @Test public void testAuthentication() { // 1.構(gòu)建SecurityManager環(huán)境 DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager(); defaultSecurityManager.setRealm(simpleAccountRealm); // 2.主體提交認(rèn)證請求 // 設(shè)置SecurityManager環(huán)境 SecurityUtils.setSecurityManager(defaultSecurityManager); // 獲取當(dāng)前主體 Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("wmyskxz", "123456"); // 登錄 subject.login(token); // subject.isAuthenticated()方法返回一個boolean值,用于判斷用戶是否認(rèn)證成功 System.out.println("isAuthenticated:" + subject.isAuthenticated()); } }
上述示例中創(chuàng)建了一個SimpleAccountRealm對象,并把初始化的賬戶信息通過addAccount方法添加進(jìn)去。
實踐中自定義Realm的方法通常是繼承AuthorizingRealm類,并實現(xiàn)其doGetAuthorizationInfo方法和doGetAuthenticationInfo方法。在上面的流程梳理過程中,我們已經(jīng)知道doGetAuthorizationInfo方法為授權(quán)功能的實現(xiàn),而doGetAuthenticationInfo方法為認(rèn)證的功能實現(xiàn)。關(guān)于具體實例,后續(xù)會用專門的實例來講解。
“Shiro認(rèn)證與授權(quán)原理是什么”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實用文章!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。