和你們分享一個鬆哥原創的 Shiro 教程吧,還沒寫完,先整一部分,剩下的敬請期待。html
Apache Shiro是一個開源安全框架,提供身份驗證、受權、密碼學和會話管理。Shiro框架具備直觀、易用等特性,同時也能提供健壯的安全性,雖然它的功能不如SpringSecurity那麼強大,可是在普通的項目中也夠用了。java
Shiro的前身是JSecurity,2004年,Les Hazlewood和Jeremy Haile創辦了Jsecurity。當時他們找不到適用於應用程序級別的合適Java安全框架,同時又對JAAS很是失望。2004年到2008年期間,JSecurity託管在SourceForge上,貢獻者包括Peter Ledbrook、Alan Ditzel和Tim Veil。2008年,JSecurity項目貢獻給了Apache軟件基金會(ASF),並被接納成爲Apache Incubator項目,由導師管理,目標是成爲一個頂級Apache項目。期間,Jsecurity曾短暫改名爲Ki,隨後因商標問題被社區改名爲「Shiro」。隨後項目持續在Apache Incubator中孵化,並增長了貢獻者Kalle Korhonen。2010年7月,Shiro社區發佈了1.0版,隨後社區建立了其項目管理委員會,並選舉Les Hazlewood爲主席。2010年9月22日,Shrio成爲Apache軟件基金會的頂級項目(TLP)。mysql
Apache Shiro是一個強大而靈活的開源安全框架,它乾淨利落地處理身份認證,受權,企業會話管理和加密。Apache Shiro的首要目標是易於使用和理解。安全有時候是很複雜的,甚至是痛苦的,但它沒有必要這樣。框架應該儘量掩蓋複雜的地方,露出一個乾淨而直觀的API,來簡化開發人員在應用程序安全上所花費的時間。git
如下是你能夠用Apache Shiro 所作的事情:github
爲沒有關聯到登陸的用戶啓用"Remember Me"服務web
等等
Apache Shiro是一個擁有許多功能的綜合性的程序安全框架。下面的圖表展現了Shiro的重點:算法
Shiro中有四大基石——身份驗證,受權,會話管理和加密。spring
除此以外,Shiro也提供了額外的功能來解決在不一樣環境下所面臨的安全問題,尤爲是如下這些:sql
要學習shiro,咱們首先需求去shiro官網下載shiro,官網地址地址https://shiro.apache.org/,截... 在 2017-2019 曾經停更了兩年,我一度覺得覺得這個項目 gg 了),本文將採用這個版本。固然,shiro咱們也能夠從github上下載到源碼。兩個源碼下載地址以下:數據庫
上面我主要是和小夥伴們介紹下源碼的下載,並無涉及到jar包的下載,jar包咱們到時候直接使用maven便可。
這裏咱們先不急着寫代碼,咱們先打開剛剛下載到的源碼,源碼中有一個samples目錄,以下:
這個samples目錄是官方給咱們的一些演示案例,其中有一個quickstart項目,這個項目是一個maven項目,參考這個quickstart,咱們來建立一個本身的演示工程。
1.首先使用maven建立一個JavaSE工程
工程建立成功後在pom文件中添加以下依賴:
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-all</artifactId> <version>RELEASE</version> </dependency>
2.配置用戶
參考quickstart項目中的shiro.ini文件,咱們來配置一個用戶,配置方式以下:首先在resources目錄下建立一個shiro.ini文件,文件內容以下:
[users] sang=123,admin [roles] admin=*
以上配置表示咱們建立了一個名爲sang的用戶,該用戶的密碼是123,該用戶的角色是admin,而admin具備操做全部資源的權限。
3.執行登陸
OK,作完上面幾步以後,咱們就能夠來看看如何實現一次簡單的登陸操做了。這個登陸操做咱們依然是參考quickstart項目中的類來實現,首先咱們要經過shiro.ini建立一個SecurityManager,再將這個SecurityManager設置爲單例模式,以下:
Factory<org.apache.shiro.mgt.SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance(); SecurityUtils.setSecurityManager(securityManager);
如此以後,咱們就配置好了一個基本的Shiro環境,注意此時的用戶和角色信息咱們配置在shiro.ini這個配置文件中,接下來咱們就能夠獲取一個Subject了,這個Subject就是咱們當前的用戶對象,獲取方式以下:
Subject currentUser = SecurityUtils.getSubject();
拿到這個用戶對象以後,接下來咱們能夠獲取一個session了,這個session和咱們web中的HttpSession的操做基本上是一致的,不一樣的是,這個session不依賴任何容器,能夠隨時隨地獲取,獲取和操做方式以下:
//獲取session Session session = currentUser.getSession(); //給session設置屬性值 session.setAttribute("someKey", "aValue"); //獲取session中的屬性值 String value = (String) session.getAttribute("someKey");
說了這麼多,咱們的用戶到如今尚未登陸呢,Subject中有一個isAuthenticated方法用來判斷當前用戶是否已經登陸,若是isAuthenticated方法返回一個false,則表示當前用戶未登陸,那咱們就能夠執行登錄,登陸方式以下:
if (!currentUser.isAuthenticated()) { UsernamePasswordToken token = new UsernamePasswordToken("sang", "123"); try { 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 (AuthenticationException ae) { } }
首先構造UsernamePasswordToken,兩個參數就是咱們的用戶名和密碼,而後調用Subject中的login方法執行登陸,當用戶名輸錯,密碼輸錯、或者帳戶鎖定等問題出現時,系統會經過拋異常告知調用者這些問題。
當登陸成功以後,咱們能夠經過以下方式獲取當前登錄用戶的用戶名:
log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");
咱們也能夠經過調用Subject中的hasRole和isPermitted方法來判斷當前用戶是否具有某種角色或者某種權限,以下:
if (currentUser.hasRole("admin")) { log.info("May the Schwartz be with you!"); } else { log.info("Hello, mere mortal."); } 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."); }
最後,咱們能夠經過logout方法註銷本次登陸,以下:
currentUser.logout();
OK,至此,咱們經過官方案例給小夥伴們簡單介紹了Shiro中的登陸操做,完整案例你們能夠參考官方的demo。
首先咱們來看shiro官方文檔中這樣一張登陸流程圖:
參照此圖,咱們的登陸一共要通過以下幾個步驟:
OK,經過上面的介紹,相信小夥伴們對整個登陸流程都有必定的理解了,小夥伴能夠經過打斷點來驗證咱們上文所說的五個步驟。那麼在上面的五個步驟中,小夥伴們看到了有一個Realm承擔了很重要的一部分工做,那麼這個Realm究竟是個什麼東西,接下來咱們就來仔細看一看。
根據Realm文檔上的解釋,Realms擔當Shiro和你的應用程序的安全數據之間的「橋樑」或「鏈接器」。當它實際上與安全相關的數據如用來執行身份驗證(登陸)及受權(訪問控制)的用戶賬戶交互時,Shiro從一個或多個爲應用程序配置的Realm 中尋找許多這樣的東西。在這個意義上說,Realm 本質上是一個特定安全的DAO:它封裝了數據源的鏈接詳細信息,使Shiro 所需的相關的數據可用。當配置Shiro 時,你必須指定至少一個Realm 用來進行身份驗證和/或受權。SecurityManager可能配置多個Realms,但至少有一個是必須的。Shiro 提供了當即可用的Realms 來鏈接一些安全數據源(即目錄),如LDAP,關係數據庫(JDBC),文本配置源,像INI 及屬性文件,以及更多。你能夠插入你本身的Realm 實現來表明自定義的數據源,若是默認地Realm不符合你的需求。
看了上面這一段解釋,可能還有小夥伴雲裏霧裏,那麼接下來咱們來經過一個簡單的案例來看看Realm到底扮演了一個什麼樣的做用,注意,本文的案例在上文案例的基礎上完成。首先自定義一個MyRealm,內容以下:
public class MyRealm implements Realm { public String getName() { return "MyRealm"; } public boolean supports(AuthenticationToken token) { return token instanceof UsernamePasswordToken; } public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String password = new String(((char[]) token.getCredentials())); String username = token.getPrincipal().toString(); if (!"sang".equals(username)) { throw new UnknownAccountException("用戶不存在"); } if (!"123".equals(password)) { throw new IncorrectCredentialsException("密碼不正確"); } return new SimpleAuthenticationInfo(username, password, getName()); } }
自定義Realm實現Realm接口,該接口中有三個方法,第一個getName方法用來獲取當前Realm的名字,第二個supports方法用來判斷這個realm所支持的token,這裏我假設值只支持UsernamePasswordToken類型的token,第三個getAuthenticationInfo方法則進行了登錄邏輯判斷,從token中取出用戶的用戶名密碼等,進行判斷,固然,我這裏省略掉了數據庫操做,當登陸驗證出現問題時,拋異常便可,這裏拋出的異常,將在執行登陸那裏捕獲到(注意,因爲我這裏定義的MyRealm是實現了Realm接口,因此這裏的用戶名和密碼都須要我手動判斷是否正確,後面的文章我會介紹其餘寫法)。
OK,建立好了MyRealm以後還不夠,咱們還須要作一個簡單配置,讓MyRealm生效,將shiro.ini文件中的全部東西都註釋掉,添加以下兩行:
MyRealm= org.sang.MyRealm securityManager.realms=$MyRealm
第一行表示定義了一個realm,第二行將這個定義好的交給securityManger,這裏實際上會調用到RealmSecurityManager類的setRealms方法。OK,作好這些以後,小夥伴們能夠在MyRealm類中的一些關鍵節點打上斷點,再次執行main方法,看看整個的登陸流程。
經過查看類的繼承關係,咱們發現Realm的子類實際上有不少種,這裏咱們就來看看有表明性的幾種:
可能咱們並不知道,實際上這個類在咱們第二篇文章中就已經用過了。這個類一開始就有以下兩行定義:
public static final String USERS_SECTION_NAME = "users"; public static final String ROLES_SECTION_NAME = "roles";
這兩行配置表示shiro.ini文件中,[users]下面的表示表用戶名密碼還有角色,[roles]下面的則是角色和權限的對應關係。
PropertiesRealm則規定了另一種用戶、角色定義方式,以下:
user.user1=password,role1
role.role1=permission1
這個顧名思義,就是從數據庫中查詢用戶的角色、權限等信息。打開JdbcRealm類,咱們看到源碼中有以下幾行:
protected static final String DEFAULT_AUTHENTICATION_QUERY = "select password from users where username = ?"; protected static final String DEFAULT_SALTED_AUTHENTICATION_QUERY = "select password, password_salt from users where username = ?"; protected static final String DEFAULT_USER_ROLES_QUERY = "select role_name from user_roles where username = ?"; protected static final String DEFAULT_PERMISSIONS_QUERY = "select permission from roles_permissions where role_name = ?";
根據這幾行預設的SQL咱們就能夠大體推斷出數據庫中表的名稱以及字段了,固然,咱們也能夠自定義SQL。JdbcRealm其實是AuthenticatingRealm的子類,關於AuthenticatingRealm咱們在後面還會詳細說到,這裏先不展開。接下來咱們就來詳細說說這個JdbcRealm。
使用JdbcRealm,涉及到數據庫操做,要用到數據庫鏈接池,這裏我使用Druid數據庫鏈接池,所以首先添加以下依賴:
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>RELEASE</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.27</version> </dependency>
想要使用JdbcRealm,那我首先要建立數據庫,根據JdbcRealm中預設的SQL,我定義的數據庫表結構以下:
這裏爲了你們可以直觀的看到表的關係,我使用了外鍵,實際工做中,視狀況而定。而後向表中添加幾條測試數據。數據庫腳本小夥伴能夠在github上下載到(https://github.com/lenve/shir...
而後將shiro.ini中的全部配置註釋掉,添加以下配置:
jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm dataSource=com.alibaba.druid.pool.DruidDataSource dataSource.driverClassName=com.mysql.jdbc.Driver dataSource.url=jdbc:mysql://localhost:3306/shiroDemo dataSource.username=root dataSource.password=123 jdbcRealm.dataSource=$dataSource jdbcRealm.permissionsLookupEnabled=true securityManager.realms=$jdbcRealm
這裏的配置文件都很簡單,不作過多贅述,小夥伴惟一須要注意的是permissionsLookupEnabled須要設置爲true,不然一會JdbcRealm就不會去查詢權限用戶權限。
OK,作完上面幾步就能夠測試了,測試方式和第二篇文章中同樣,咱們能夠測試下用戶登陸,用戶角色和用戶權限。
小夥伴們看懂了上文,對於自定義查詢SQL就沒什麼問題了。我這裏舉一個簡單的例子,好比我要自定義authenticationQuery對對應的SQL,查看JdbcRealm源碼,咱們發現authenticationQuery對應的SQL原本是select password from users where username = ?
,若是須要修改的話,好比說個人表名不是users而是employee,那麼在shiro.ini中添加以下配置便可:
jdbcRealm.authenticationQuery=select password from employee where username = ?
OK,這個小夥伴下來本身作嘗試,我這裏就不演示了。
不知道小夥伴們是否還記得這張登陸流程圖:
從這張圖中咱們能夠清晰看到Realm是能夠有多個的,不過到目前爲止,咱們全部的案例都仍是單Realm,那麼咱們先來看一個簡單的多Realm狀況。
前面的文章咱們本身建立了一個MyRealm,也用過JdbcRealm,但都是單獨使用的,如今我想將兩個一塊兒使用,只須要修改shiro.ini配置便可,以下:
MyRealm= org.sang.MyRealm jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm dataSource=com.alibaba.druid.pool.DruidDataSource dataSource.driverClassName=com.mysql.jdbc.Driver dataSource.url=jdbc:mysql://localhost:3306/shiroDemo dataSource.username=root dataSource.password=123 jdbcRealm.dataSource=$dataSource jdbcRealm.permissionsLookupEnabled=true securityManager.realms=$jdbcRealm,$MyRealm
可是此時我數據庫中用戶的信息是sang/123,MyRealm中配置的信息也是sang/123,我把MyRealm中的用戶信息修改成江南一點雨/456
,此時,個人MyRealm的getAuthenticationInfo方法以下:
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String password = new String(((char[]) token.getCredentials())); String username = token.getPrincipal().toString(); if (!"江南一點雨".equals(username)) { throw new UnknownAccountException("用戶不存在"); } if (!"456".equals(password)) { throw new IncorrectCredentialsException("密碼不正確"); } return new SimpleAuthenticationInfo(username, password, getName()); }
這個時候咱們就配置了兩個Realm,仍是使用咱們一開始的測試代碼進行登陸測試,這個時候咱們發現我既可使用江南一點雨/456
進行登陸,也可使用sang/123
進行登陸,用sang/123
登陸成功以後用戶的角色信息和以前是同樣的,而用江南一點雨/456
登陸成功以後用戶沒有角色,這個也很好理解,由於咱們在MyRealm中沒有給用戶配置任何權限。總而言之,就是當我有了兩個Realm以後,如今只須要這兩個Realm中的任意一個認證成功,就算我當前用戶認證成功。
好了,有了上面的問題後,接下來咱們在Subject的login方法上打斷點,跟隨程序的執行步驟,咱們來到了ModularRealmAuthenticator類的doMultiRealmAuthentication方法中,以下:
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { this.assertRealmsConfigured(); Collection<Realm> realms = this.getRealms(); return realms.size() == 1?this.doSingleRealmAuthentication((Realm)realms.iterator().next(), authenticationToken):this.doMultiRealmAuthentication(realms, authenticationToken); }
在這個方法中,首先會獲取當前一共有多少個realm,若是隻有一個則執行doSingleRealmAuthentication方法進行處理,若是有多個realm,則執行doMultiRealmAuthentication方法進行處理。doSingleRealmAuthentication方法部分源碼以下:
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) { ... ... 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); } else { return info; } }
小夥伴們看到這裏就明白了,這裏調用了realm的getAuthenticationInfo方法,這個方法實際上就是咱們本身實現的MyRealm中的getAuthenticationInfo方法。
那若是有多個Realm呢?咱們來看看doMultiRealmAuthentication方法的實現,部分源碼以下:
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) { AuthenticationStrategy strategy = this.getAuthenticationStrategy(); AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token); Iterator var5 = realms.iterator(); while(var5.hasNext()) { Realm realm = (Realm)var5.next(); aggregate = strategy.beforeAttempt(realm, token, aggregate); if(realm.supports(token)) { AuthenticationInfo info = null; Throwable t = null; try { info = realm.getAuthenticationInfo(token); } catch (Throwable var11) { } 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; }
我這裏主要來講下這個方法的實現思路:
OK,通過上面的簡單解析,小夥伴們對認證策略應該有一個大體的認識了,那麼在Shiro中,一共支持三種不一樣的認證策略,以下:
配置方式也很簡單,在shiro.ini中進行配置,在上面配置的基礎上,增長以下配置:
authenticator=org.apache.shiro.authc.pam.ModularRealmAuthenticator securityManager.authenticator=$authenticator allSuccessfulStrategy=org.apache.shiro.authc.pam.AllSuccessfulStrategy securityManager.authenticator.authenticationStrategy=$allSuccessfulStrategy
此時,咱們再進行登陸測試,則會要求每一個Realm都認證經過纔算認證經過。
2011年12月21日,有人在網絡上公開了一個包含600萬個CSDN用戶資料的數據庫,數據所有爲明文儲存,包含用戶名、密碼以及註冊郵箱。事件發生後CSDN在微博、官方網站等渠道發出了聲明,解釋說此數據庫系2009年備份所用,因不明緣由泄露,已經向警方報案。後又在官網網站發出了公開道歉信。在接下來的十多天裏,金山、網易、京東、噹噹、新浪等多家公司被捲入到此次事件中。整個事件中最觸目驚心的莫過於CSDN把用戶密碼明文存儲,因爲不少用戶是多個網站共用一個密碼,所以一個網站密碼泄露就會形成很大的安全隱患。因爲有了這麼多前車可鑑,咱們如今作系統時,密碼都要加密處理。
密碼加密咱們通常會用到散列函數,又稱散列算法、哈希函數,是一種從任何一種數據中建立小的數字「指紋」的方法。散列函數把消息或數據壓縮成摘要,使得數據量變小,將數據的格式固定下來。該函數將數據打亂混合,從新建立一個叫作散列值的指紋。散列值一般用一個短的隨機字母和數字組成的字符串來表明。好的散列函數在輸入域中不多出現散列衝突。在散列表和數據處理中,不抑制衝突來區別數據,會使得數據庫記錄更難找到。咱們經常使用的散列函數有以下幾種:
MD5消息摘要算法是一種被普遍使用的密碼散列函數,能夠產生出一個128位(16字節)的散列值,用於確保信息傳輸完整一致。MD5由美國密碼學家羅納德·李維斯特設計,於1992年公開,用以取代MD4算法。這套算法的程序在 RFC 1321中被加以規範。將數據(如一段文字)運算變爲另外一固定長度值,是散列算法的基礎原理。1996年後被證明存在弱點,能夠被加以破解,對於須要高度安全性的數據,專家通常建議改用其餘算法,如SHA-2。2004年,證明MD5算法沒法防止碰撞,所以不適用於安全性認證,如SSL公開密鑰認證或是數字簽名等用途。
安全散列算法(Secure Hash Algorithm)是一個密碼散列函數家族,是FIPS所認證的安全散列算法。能計算出一個數字消息所對應到的,長度固定的字符串(又稱消息摘要)的算法。且若輸入的消息不一樣,它們對應到不一樣字符串的機率很高。SHA家族的算法,由美國國家安全局所設計,並由美國國家標準與技術研究院發佈,是美國的政府標準,其分別是:SHA-0:1993年發佈,是SHA-1的前身;SHA-1:1995年發佈,SHA-1在許多安全協議中廣爲使用,包括TLS和SSL、PGP、SSH、S/MIME和IPsec,曾被視爲是MD5的後繼者。但SHA-1的安全性在2000年之後已經不被大多數的加密場景所接受。2017年荷蘭密碼學研究小組CWI和Google正式宣佈攻破了SHA-1;SHA-2:2001年發佈,包括SHA-22四、SHA-25六、SHA-38四、SHA-5十二、SHA-512/22四、SHA-512/256。雖然至今還沒有出現對SHA-2有效的攻擊,它的算法跟SHA-1基本上仍然類似;所以有些人開始發展其餘替代的散列算法;SHA-3:2015年正式發佈,SHA-3並非要取代SHA-2,由於SHA-2目前並無出現明顯的弱點。因爲對MD5出現成功的破解,以及對SHA-0和SHA-1出現理論上破解的方法,NIST感受須要一個與以前算法不一樣的,可替換的加密散列算法,也就是如今的SHA-3。
Shiro中對以上兩種散列算法都提供了支持,對於MD5,Shiro中生成消息摘要的方式以下:
Md5Hash md5Hash = new Md5Hash("123", null, 1024);
第一個參數是要生成密碼的明文,第二個參數密碼的鹽值,第三個參數是生成消息摘要的迭代次數。
Shiro中對於安全散列算法的支持以下(支持多種算法,這裏我舉一個例子):
Sha512Hash sha512Hash = new Sha512Hash("123", null, 1024);
這裏三個參數含義與上文基本一致,再也不贅述。shiro中也提供了通用的算法,以下:
SimpleHash md5 = new SimpleHash("md5", "123", null, 1024); SimpleHash sha512 = new SimpleHash("sha-512", "123", null, 1024);
當用戶註冊時,咱們能夠經過上面的方式對密碼進行加密,將加密後的字符串存入數據庫中。我這裏爲了簡單,就不寫註冊功能了,就把昨天數據庫中用戶的密碼123改爲sha512所對應的字符串,以下:
cb5143cfcf5791478e057be9689d2360005b3aac951f947af1e6e71e3661bf95a7d14183dadfb0967bd6338eb4eb2689e9c227761e1640e6a033b8725fabc783
同時,爲了不其餘Realm的干擾,數據庫中我只配置一個JdbcRealm。
此時若是我不作其餘修改的話,登陸必然會失敗,緣由很簡單:我登陸時輸入的密碼是123,可是數據庫中的密碼是一個很長的字符串,因此登陸確定不會成功。經過打斷點,咱們發現最終的密碼比對是在SimpleCredentialsMatcher類中的doCredentialsMatch方法中進行密碼比對的,比對的方式也很簡單,直接使用了對用戶輸入的密碼和數據庫中的密碼生成byte數組而後進行比較,最終的比較在MessageDigest類的isEqual方法中。部分邏輯以下:
protected boolean equals(Object tokenCredentials, Object accountCredentials) { ... ... //獲取用戶輸入密碼的byte數組 byte[] tokenBytes = this.toBytes(tokenCredentials); //獲取數據庫中密碼的byte數組 byte[] accountBytes = this.toBytes(accountCredentials); return MessageDigest.isEqual(tokenBytes, accountBytes); ... }
MessageDigest的isEqual方法以下:
public static boolean isEqual(byte[] digesta, byte[] digestb) { if (digesta == digestb) return true; if (digesta == null || digestb == null) { return false; } if (digesta.length != digestb.length) { return false; } int result = 0; // time-constant comparison for (int i = 0; i < digesta.length; i++) { result |= digesta[i] ^ digestb[i]; } return result == 0; }
都是很容易理解的比較代碼,這裏不贅述。咱們如今之因此登陸失敗是由於沒有對用戶輸入的密碼進行加密,經過對源代碼的分析,咱們發現是由於在AuthenticatingRealm類的assertCredentialsMatch方法中獲取了一個名爲SimpleCredentialsMatcher的密碼比對器,這個密碼比對器中比對的方法就是簡單的比較,所以若是咱們可以將這個密碼比對器換掉就行了。咱們來看一下CredentialsMatcher的繼承關係:
咱們發現這個恰好有一個Sha512CredentialsMatcher比對器,這個比對器的doCredentialsMatch方法在它的父類HashedCredentialsMatcher,方法內容以下:
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { Object tokenHashedCredentials = hashProvidedCredentials(token, info); Object accountCredentials = getCredentials(info); return equals(tokenHashedCredentials, accountCredentials); }
這時咱們發現獲取tokenHashedCredentials的方式不像之前那樣簡單粗暴了,而是調用了hashProvidedCredentials方法,而hashProvidedCredentials方法最終會來到下面這個重載方法中:
protected Hash hashProvidedCredentials(Object credentials, Object salt, int hashIterations) { String hashAlgorithmName = assertHashAlgorithmName(); return new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations); }
這幾行代碼似曾相識,很明顯,是系統幫咱們對用戶輸入的密碼進行了轉換。瞭解了這些以後,那我只須要將shiro.ini修改爲以下樣子便可實現登陸了:
sha512=org.apache.shiro.authc.credential.Sha512CredentialsMatcher # 迭代次數 sha512.hashIterations=1024 jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm dataSource=com.alibaba.druid.pool.DruidDataSource dataSource.driverClassName=com.mysql.jdbc.Driver dataSource.url=jdbc:mysql://localhost:3306/shiroDemo dataSource.username=root dataSource.password=123 jdbcRealm.dataSource=$dataSource jdbcRealm.permissionsLookupEnabled=true # 修改JdbcRealm中的credentialsMatcher屬性 jdbcRealm.credentialsMatcher=$sha512 securityManager.realms=$jdbcRealm
如此以後,咱們再進行登陸測試,就能夠登陸成功了。
本小節案例下載:https://github.com/lenve/shir...
無論是消息摘要算法仍是安全散列算法,若是原文同樣,生成密文也是同樣的,這樣的話,若是兩個用戶的密碼原文同樣,存到數據庫中密文也就同樣了,仍是不安全,咱們須要作進一步處理,常看法決方案就是加鹽。鹽從那裏來呢?咱們可使用用戶id(由於通常狀況下,用戶id是惟一的),也可使用一個隨機字符,我這裏採用第一種方案。
shiro中加鹽的方式很簡單,在用戶註冊時生成密碼密文時,就要加入鹽,以下幾種方式:
Md5Hash md5Hash = new Md5Hash("123", "sang", 1024); Sha512Hash sha512Hash = new Sha512Hash("123", "sang", 1024); SimpleHash md5 = new SimpleHash("md5", "123", "sang", 1024); SimpleHash sha512 = new SimpleHash("sha-512", "123", "sang", 1024)
而後咱們首先將sha512生成的字符串放入數據庫中,接下來我要配置一下個人jdbcRealm,由於我要指定個人鹽是什麼。在這裏個人鹽就是個人用戶名,每一個用戶的用戶名是不同的,所以這裏無法寫死,在JdbcRealm中,系統提供了四種不一樣的SaltStyle,以下:
SaltStyle | 含義 |
---|---|
NO_SALT | 默認,密碼不加鹽 |
CRYPT | 密碼是以Unix加密方式儲存的 |
COLUMN | salt是單獨的一列儲存在數據庫中 |
EXTERNAL | salt沒有儲存在數據庫中,須要經過JdbcRealm.getSaltForUser(String)函數獲取 |
四種不一樣的SaltStyle對應了四種不一樣的密碼處理方式,部分源碼以下:
switch (saltStyle) { case NO_SALT: password = getPasswordForUser(conn, username)[0]; break; case CRYPT: // TODO: separate password and hash from getPasswordForUser[0] throw new ConfigurationException("Not implemented yet"); //break; case COLUMN: String[] queryResults = getPasswordForUser(conn, username); password = queryResults[0]; salt = queryResults[1]; break; case EXTERNAL: password = getPasswordForUser(conn, username)[0]; salt = getSaltForUser(username); }
在COLUMN這種狀況下,SQL查詢結果應該包含兩列,第一列是密碼,第二列是鹽,這裏默認執行的SQL在JdbcRealm一開頭就定義好了,以下:
protected static final String DEFAULT_SALTED_AUTHENTICATION_QUERY = "select password, password_salt from users where username = ?";
即系統默認的鹽是數據表中的password_salt提供的,可是我這裏是username字段提供的,因此這裏我一會要自定義這條SQL。自定義方式很簡單,修改shiro.ini文件,添加以下兩行:
jdbcRealm.saltStyle=COLUMN jdbcRealm.authenticationQuery=select password,username from users where username=?
首先設置saltStyle爲COLUMN,而後從新定義authenticationQuery對應的SQL。注意返回列的順序很重要,不能隨意調整。如此以後,系統就會自動把username字段做爲鹽了。
不過,因爲ini文件中不支持枚舉,saltStyle的值其實是一個枚舉類型,因此咱們在測試的時候,須要增長一個枚舉轉換器在咱們的main方法中,以下:
BeanUtilsBean.getInstance().getConvertUtils().register(new AbstractConverter() { @Override protected String convertToString(Object value) throws Throwable { return ((Enum) value).name(); } @Override protected Object convertToType(Class type, Object value) throws Throwable { return Enum.valueOf(type, value.toString()); } @Override protected Class getDefaultType() { return null; } }, JdbcRealm.SaltStyle.class);
固然,之後當咱們將shiro和web項目整合以後,就不須要這個轉換器了。
如此以後,咱們就能夠再次進行登陸測試了,會發現沒什麼問題了。
OK,剛剛是在JdbcRealm中配置了鹽,若是沒用JdbcRealm,而是本身定義的普通Realm,要怎麼解決配置鹽的問題?
首先要說明一點是,咱們前面的文章在自定義Realm時都是經過實現Realm接口實現的,這種方式有一個缺陷,就是密碼比對須要咱們本身完成,通常在項目中,咱們自定義Realm都是經過繼承AuthenticatingRealm或者AuthorizingRealm,由於這兩個方法中都重寫了getAuthenticationInfo方法,而在getAuthenticationInfo方法中,調用doGetAuthenticationInfo方法獲取登陸用戶,獲取到以後,會調用assertCredentialsMatch方法進行密碼比對,而咱們直接實現Realm接口則沒有這一步,部分源碼以下:
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { AuthenticationInfo info = getCachedAuthenticationInfo(token); if (info == null) { //調用doGetAuthenticationInfo獲取info,這個doGetAuthenticationInfo是咱們在自定義Realm中本身實現的 info = doGetAuthenticationInfo(token); log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info); if (token != null && info != null) { cacheAuthenticationInfoIfPossible(token, info); } } else { log.debug("Using cached authentication info [{}] to perform credentials matching.", info); } if (info != null) { //獲取到info以後,進行密碼比對 assertCredentialsMatch(token, info); } else { log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token); } return info; }
基於上面所述的緣由,這裏我先繼承AuthenticatingRealm,以下:
public class MyRealm extends AuthenticatingRealm { public String getName() { return "MyRealm"; } public boolean supports(AuthenticationToken token) { return token instanceof UsernamePasswordToken; } protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String username = token.getPrincipal().toString(); if (!"sang".equals(username)) { throw new UnknownAccountException("用戶不存在"); } String dbPassword = "a593ccad1351a26cf6d91d5f0f24234c6a4da5cb63208fae56fda809732dcd519129acd74046a1f9c5992db8903f50ebf3c1091b3aaf67a05c82b7ee470d9e58"; return new SimpleAuthenticationInfo(username, dbPassword, ByteSource.Util.bytes(username), getName()); } }
關於這個類,我說以下幾點:
上面的核心步驟是第三步,系統去自動比較密碼輸入是否正確,在比對的過程當中,須要首先對用戶輸入的密碼進行加鹽加密,既然加鹽加密,就會涉及到credentialsMatcher,這裏咱們要用的credentialsMatcher實際上和在JdbcRealm中用的credentialsMatcher同樣,只須要在配置文件中增長以下一行便可:
MyRealm.credentialsMatcher=$sha512
sha512和咱們上文定義的一致,這裏就再也不重複說了。
本小節案例下載:https://github.com/lenve/shir...
密碼加密加鹽小夥伴們應該沒有問題了,可是前面幾篇文章又給咱們帶來了一個新的問題:咱們前面IniRealm、JdbcRealm以及自定義的MyRealm,其中前兩個咱們都能實現用戶認證以及受權,即既能管理用戶登陸,又能管理用戶角色,而咱們自定義的MyRealm,目前還只能實現登陸,不能實現受權,本文咱們就來看看自定義Realm如何實現受權。
上篇文章咱們沒有實現自定義Realm的受權操做,可是這個並不影響咱們調用hasRole方法去獲取用戶的權限,我在上文測試代碼上的currentUser.hasRole上面打斷點,經過層層追蹤,咱們發現最終來到了ModularRealmAuthorizer類的hasRole方法中,部分源碼以下:
public boolean hasRole(PrincipalCollection principals, String roleIdentifier) { assertRealmsConfigured(); for (Realm realm : getRealms()) { if (!(realm instanceof Authorizer)) continue; if (((Authorizer) realm).hasRole(principals, roleIdentifier)) { return true; } } return false; }
咱們看到在這裏會遍歷全部的realm,若是這個realm是Authorizer的實例,則會進行進一步的受權操做,若是不是Authorizer的實例,則直接跳過,而咱們只有一個自定義的MyRealm繼承自AuthenticatingRealm,很明顯不是Authorizer的實例,因此這裏必然返回false,受權失敗,因此要解決受權問題,第一步,得先讓咱們的MyRealm成爲Authorizer的實例。
以下圖是Authorizer的繼承關係:
小夥伴們看到,在Authorizer的實現類中有一個AuthorizingRealm,打開這個類,咱們發現它的繼承關係以下:
public abstract class AuthorizingRealm extends AuthenticatingRealm implements Authorizer, Initializable, PermissionResolverAware, RolePermissionResolverAware { ... }
咱們發現,這個AuthorizingRealm不只是Authorizer的實現類,同時也是咱們上文所用的AuthenticatingRealm的實現類,既然AuthorizingRealm同時是這兩個類的實現類,那麼我把MyRealm的繼承關係由AuthenticatingRealm改成AuthorizingRealm,確定不會影響我上文的功能,修改以後的MyRealm以下(部分關鍵代碼):
public class MyRealm extends AuthorizingRealm { protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String username = token.getPrincipal().toString(); if (!"sang".equals(username)) { throw new UnknownAccountException("用戶不存在"); } String dbPassword = "a593ccad1351a26cf6d91d5f0f24234c6a4da5cb63208fae56fda809732dcd519129acd74046a1f9c5992db8903f50ebf3c1091b3aaf67a05c82b7ee470d9e58"; return new SimpleAuthenticationInfo(username, dbPassword, ByteSource.Util.bytes(username), getName()); } protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { Set<String> roles = new HashSet<String>(); if ("sang".equals(principals.getPrimaryPrincipal().toString())) { roles.add("普通用戶"); } return new SimpleAuthorizationInfo(roles); } }
繼承了AuthorizingRealm以後,須要咱們實現doGetAuthorizationInfo方法。在這個方法中,咱們配置用戶的權限。這裏我爲了方便,直接添加了普通用戶這個權限,實際上,這裏應該根據用戶名去數據庫裏查詢權限,查詢方式不贅述。
經過源碼追蹤,咱們發現最終受權會來到AuthorizingRealm類的以下兩個方法中:
public boolean hasRole(PrincipalCollection principal, String roleIdentifier) { AuthorizationInfo info = getAuthorizationInfo(principal); return hasRole(roleIdentifier, info); } protected boolean hasRole(String roleIdentifier, AuthorizationInfo info) { return info != null && info.getRoles() != null && info.getRoles().contains(roleIdentifier); }
這兩個方法的邏輯很簡單,第一個方法中調用的getAuthorizationInfo方法會最終調用到咱們自定義的doGetAuthorizationInfo方法,第二個hasRole方法接收的兩個參數,第一個是用戶申請的角色,第二個是用戶具有的角色集,一個簡單的contains函數就判斷出用戶是否具有某個角色了。
可是這個時候,用戶只有角色,沒有權限,咱們能夠對doGetAuthorizationInfo方法作進一步的完善,以下:
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { Set<String> roles = new HashSet<String>(); Set<String> permiss = new HashSet<String>(); if ("sang".equals(principals.getPrimaryPrincipal().toString())) { roles.add("普通用戶"); permiss.add("book:update"); } SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles); info.setStringPermissions(permiss); return info; }
固然,正常狀況下,權限也應當是從數據庫中查詢獲得的,我這裏簡化下。
那麼這個角色是怎麼驗證的呢?追蹤源碼咱們來到了AuthorizingRealm類的以下兩個方法中:
public boolean isPermitted(PrincipalCollection principals, Permission permission) { AuthorizationInfo info = getAuthorizationInfo(principals); return isPermitted(permission, info); } //visibility changed from private to protected per SHIRO-332 protected boolean isPermitted(Permission permission, AuthorizationInfo info) { Collection<Permission> perms = getPermissions(info); if (perms != null && !perms.isEmpty()) { for (Permission perm : perms) { if (perm.implies(permission)) { return true; } } } return false; }
第一個isPermitted方法中調用了getAuthorizationInfo方法,而getAuthorizationInfo方法最終會調用到咱們本身定義的doGetAuthorizationInfo方法,即獲取到用戶的角色權限信息,而後在第二個方法中進行遍歷判斷,查看是否具有相應的權限,第二個isPermitted方法的第一個參數就是用戶要申請的權限。
本小節案例下載:https://github.com/lenve/shir...
Spring和SpringMVC環境的搭建,總體上來講,仍是比較容易的,由於這個不是本文的重點,所以這裏我不作詳細介紹,小夥伴能夠在文末下載源碼查看Spring+SpringMVC環境的搭建。同時,因爲MyBatis的整合相對要容易不少,這裏爲了下降項目複雜度,我也就先不引入MyBatis。
對於項目依賴,除了Spring、SpringMVC、Shiro相關的依賴,還須要加入Shiro和Spring整合的jar,以下:
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>RELEASE</version> </dependency>
搭建好Spring+SpringMVC環境以後,整合Shiro咱們主要配置兩個地方:
<filter> <filter-name>shiroFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <init-param> <param-name>targetFilterLifecycle</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
這樣以後,當DelegatingFilterProxy攔截到全部請求以後,都會委託給shiroFilter來處理,shiroFilter是咱們第二步在Spring容器中配置的一個實例。
在Spring容器中至少有兩個Bean須要咱們配置,一個就是第一步中的shiroFilter,還有一個就是SecurityManager,完整配置以下:
<bean class="org.apache.shiro.web.mgt.DefaultWebSecurityManager" id="securityManager"> </bean> <bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" id="shiroFilter"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login.jsp"></property> <property name="successUrl" value="/success.jsp"/> <property name="unauthorizedUrl" value="/unauthorized.jsp"/> <property name="filterChainDefinitions"> <value> /**=authc </value> </property> </bean>
這是一個很是簡單的配置,咱們在之後的文章中還會繼續完善它,關於這個配置我說以下幾點:
/**=authc
表示全部的頁面都須要認證(登陸)以後才能訪問。/a/b/*=anon /a/**=authc
假設個人路徑是/a/b/c那麼就會匹配到第一個過濾器anon,而不會匹配到authc,因此這裏的順序很重要。
OK,這些配置寫完後,在webpap目錄下建立對應的jsp文件,以下:
此時,啓動項目去瀏覽器中訪問,不管咱們訪問什麼地址,最後都會回到login.jsp頁面,由於全部的頁面(即便不存在的地址)都須要認證後才能夠訪問。
本小節案例:https://github.com/lenve/shir...
很明顯,無論是那種登陸,都離不開數據庫,這裏數據庫我採用咱們前面的數據庫,這裏不作贅述(文末能夠下載數據庫腳本),可是我這裏須要首先配置JdbcRealm,在applicationContext.xml中首先配置數據源,以下:
<context:property-placeholder location="classpath:db.properties"/> <bean class="com.alibaba.druid.pool.DruidDataSource" id="dataSource"> <property name="username" value="${db.username}"/> <property name="password" value="${db.password}"/> <property name="url" value="${db.url}"/> </bean>
有了數據源以後,接下來配置JdbcRealm,以下:
<bean class="org.apache.shiro.realm.jdbc.JdbcRealm" id="jdbcRealm"> <property name="dataSource" ref="dataSource"/> <property name="credentialsMatcher"> <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"> <property name="hashAlgorithmName" value="sha-512"/> <property name="hashIterations" value="1024"/> </bean> </property> <property name="saltStyle" value="COLUMN"/> <property name="authenticationQuery" value="select password, username from users where username = ?"/> </bean>
JdbcRealm中這幾個屬性和咱們本系列第七篇文章基本是一致的,首先咱們配置了密碼比對器爲HashedCredentialsMatcher,相應的算法爲sha512,密碼加密迭代次數爲1024次,而後咱們配置了密碼的鹽從數據表的列中來,username列就是咱們的鹽,這些配置和前文都是一致的,不清楚的小夥伴能夠參考咱們本系列第七篇文章。
自定義登陸邏輯比較簡單,首先咱們把login.jsp頁面進行簡單改造:
<form action="/login" method="post"> <table> <tr> <td>用戶名:</td> <td><input type="text" name="username"></td> </tr> <tr> <td>密碼:</td> <td><input type="password" name="password"></td> </tr> <tr> <td colspan="2"><input type="submit" value="登陸"></td> </tr> </table> </form>
而後建立咱們的登陸處理Controller,以下:
@PostMapping("/login") public String login(String username, String password) { Subject currentUser = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(username, password); try { currentUser.login(token); return "success"; } catch (AuthenticationException e) { } return "login"; }
登陸成功咱們就去success頁面,登陸失敗就回到登陸頁面。作完這兩步以後,咱們還要修改shiroFilter中的filterChainDefinitions屬性,要設置/login
接口能夠匿名訪問,以下:
<bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" id="shiroFilter"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login.jsp"></property> <property name="successUrl" value="/success.jsp"/> <property name="unauthorizedUrl" value="/unauthorized.jsp"/> <property name="filterChainDefinitions"> <value> /login=anon /**=authc </value> </property> </bean>
作完這些以後,就能夠去login.jsp頁面測試登陸了。
上面中方式是咱們本身寫登陸邏輯,shiro也給咱們提供了兩種不用本身寫登陸邏輯的登陸方式,請繼續往下看。
shiro中也提供了基於http協議的認證,固然,這種認證也得有數據庫的輔助,數據配置和前文同樣,咱們只須要修改一個配置便可,以下:
<bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" id="shiroFilter"> <property name="securityManager" ref="securityManager"/> <property name="filterChainDefinitions"> <value> /**=authcBasic </value> </property> </bean>
這個表示全部的頁面都要通過基於http的認證。此時咱們打開任意一個頁面,認證方式以下:
表單登陸和基於HTTP的登陸相似,都是不須要咱們本身寫登陸邏輯的登陸,可是出錯的邏輯仍是要稍微處理下,首先修改shiroFilter:
<bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" id="shiroFilter"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login"/> <property name="successUrl" value="/success.jsp"/> <property name="filterChainDefinitions"> <value> /**=authc </value> </property> </bean>
配置登陸頁面,也配置登陸成功後的跳轉頁面,同時設置全部頁面都要登陸後才能訪問。
配置登陸頁面請求,以下:
@RequestMapping("/login") public String login(HttpServletRequest req, Model model) { String shiroLoginFailure = (String) req.getAttribute("shiroLoginFailure"); if (UnknownAccountException.class.getName().equals(shiroLoginFailure)) { model.addAttribute("error", "帳戶不存在!"); } if (IncorrectCredentialsException.class.getName().equals(shiroLoginFailure)) { model.addAttribute("error", "密碼不正確!"); } return "login"; }
若是登陸失敗,那麼在request中會有一個shiroLoginFailure的屬性中保存了登陸失敗的異常類名,經過判斷這個類名,咱們就能夠知道是什麼緣由致使了登陸失敗。
OK,配置好這兩步以後,就能夠去登陸頁面測試了。
註銷登陸比較簡單,就一個過濾器,按以下方式配置:
<property name="filterChainDefinitions"> <value> /logout=logout /**=authc </value> </property>
經過get請求訪問/logout
便可註銷登陸。
本小節有三個案例,下載地址以下:
本文的案例在上文的基礎上完成,所以Realm這一塊我依然採用JdbcRealm,相關的受權就沒必要配置了。可是這裏的數據庫腳本有更新,小夥伴須要下載從新執行(https://github.com/lenve/shir...
先來介紹下目前數據庫中用戶的狀況,數據庫中有兩個用戶,sang具備admin的角色,同時具備book:*
和author:create
兩個權限,lisi具備user的角色,同時具備user:info
和user:delete
兩個權限。修改shiroFilter,以下:
<bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" id="shiroFilter"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login"/> <property name="successUrl" value="/success.jsp"/> <property name="unauthorizedUrl" value="/unauthorized.jsp"/> <property name="filterChainDefinitions"> <value> /admin.jsp=authc,roles[admin] /user.jsp=authc,roles[user] /logout=logout /**=authc </value> </property> </bean>
關於這裏的配置,我說以下幾點:
測試時咱們分別用sang/123和lisi/123進行登陸,登陸成功後分別訪問user.jsp和admin.jsp就能看到效果。
上面的方式是配置角色,可是尚未配置權限,要配置權限,首先要在jdbcRealm中添加容許權限信息的查詢:
<property name="permissionsLookupEnabled" value="true"/>
而後配置下shiroFilter:
<property name="filterChainDefinitions"> <value> /admin.jsp=authc,roles[admin] /user.jsp=authc,roles[user] /userinfo.jsp=authc,perms[user:info] /bookinfo.jsp=authc,perms[book:info] /logout=logout /**=authc </value> </property>
這裏假設訪問userinfo.jsp須要user:info權限,訪問bookinfo.jsp須要book:info權限。
OK,作完這些以後就能夠測試了,分別用sang/123和lisi/123進行登陸,登陸成功後分別訪問bookinfo.jsp和userinfo.jsp就能夠看到不一樣效果了。
本小節案例下載:https://github.com/lenve/shir...
上篇文章中,咱們在success.jsp中寫了不少像下面這種超連接:
<h1>登陸成功!</h1> <h3><a href="/logout">註銷</a></h3> <h3><a href="/admin.jsp">admin.jsp</a></h3> <h3><a href="/user.jsp">user.jsp</a></h3> <h3><a href="/bookinfo.jsp">bookinfo.jsp</a></h3> <h3><a href="/userinfo.jsp">userinfo.jsp</a></h3>
可是對於不一樣身份的用戶,並非每個連接都是有效的,點擊無效的連接會進入到未受權的頁面,這樣用戶體驗並很差,最好可以把不可達的連接自動隱藏起來,同時,我也但願可以方便獲取當前登陸用戶的信息等,考慮到這些需求,咱們來聊聊shiro中的jsp標籤。
shiro中的標籤並很少,主要有以下幾種:
shiro:guest標籤只有在當前未登陸時顯示裏邊的內容,以下:
<shiro:guest> 歡迎【遊客】訪問! </shiro:guest>
shiro:user是在用戶登陸以後顯示該標籤中的內容,不管是經過正常的登陸仍是經過Remember Me登陸,以下:
<shiro:user> 歡迎【<shiro:principal/>】訪問! </shiro:user>
shiro:principal用來獲取當前登陸用戶的信息,顯示效果以下:
4.shiro:authenticated
和shiro:user相比,shiro:authenticated的範圍變小,當用戶認證成功且不是經過Remember Me認證成功,這個標籤中的內容纔會顯示出來:
<shiro:authenticated> 用戶【<shiro:principal/>】身份認證經過,不是經過Remember Me認證! </shiro:authenticated>
shiro:notAuthenticated也是在用戶未認證的狀況下顯示內容,和shiro:guest不一樣的是,對於經過Remember Me方式進行的認證,shiro:guest不會顯示內容,而shiro:notAuthenticated會顯示內容(由於此時並非遊客,可是又確實未認證),以下:
<shiro:notAuthenticated> 用戶未進行身份認證 </shiro:notAuthenticated>
當用戶不具有某個角色時候,顯示內容,以下:
<shiro:lacksRole name="admin"> 用戶不具有admin角色 </shiro:lacksRole>
當用戶不具有某個權限時顯示內容:
<shiro:lacksPermission name="book:info"> 用戶不具有book:info權限 </shiro:lacksPermission>
當用戶具有某個角色時顯示的內容:
<shiro:hasRole name="admin"> <h3><a href="/admin.jsp">admin.jsp</a></h3> </shiro:hasRole>
當用戶具有多個角色中的某一個時顯示的內容:
<shiro:hasAnyRoles name="user,aaa"> <h3><a href="/user.jsp">user.jsp</a></h3> </shiro:hasAnyRoles>
當用戶具有某一個權限時顯示的內容:
<shiro:hasPermission name="book:info"> <h3><a href="/bookinfo.jsp">bookinfo.jsp</a></h3> </shiro:hasPermission>
本小節案例下載:https://github.com/lenve/shir...
使用緩存,首先須要添加相關依賴,以下:
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.4.0</version> </dependency>
ehcache的配置文件主要參考官方的配置,在resources目錄下建立ehcache.xml文件,內容以下:
<ehcache> <diskStore path="java.io.tmpdir/shiro-spring-sample"/> <defaultCache maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="false" diskPersistent="false" diskExpiryThreadIntervalSeconds="120" /> <cache name="shiro-activeSessionCache" maxElementsInMemory="10000" eternal="true" overflowToDisk="true" diskPersistent="true" diskExpiryThreadIntervalSeconds="600"/> <cache name="org.apache.shiro.realm.SimpleAccountRealm.authorization" maxElementsInMemory="100" eternal="false" timeToLiveSeconds="600" overflowToDisk="false"/> </ehcache>
這些都是ehcache緩存中常規的配置,含義我就不一一解釋了,文末下載源碼有註釋。
接下來咱們只須要在applicationContext中簡單配置下緩存便可,配置方式以下:
<bean class="org.apache.shiro.cache.ehcache.EhCacheManager" id="cacheManager"> <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/> </bean> <bean class="org.apache.shiro.web.mgt.DefaultWebSecurityManager" id="securityManager"> <property name="realm" ref="jdbcRealm"/> <property name="cacheManager" ref="cacheManager"/> </bean>
首先配置EhCacheManager類,指定緩存位置,而後在DefaultWebSecurityManager中引入cacheManager便可,如此以後,咱們的緩存就應用上了。
因爲我這裏使用了JdbcRealm,若是使用了自定義Realm那麼能夠經過打日誌看是否使用了緩存,使用了JdbcRealm以後,咱們能夠經過打斷點來查看是否應用了緩存,好比我執行以下代碼:
subject.checkRole("admin"); subject.checkPermission("book:info");
經過斷點跟蹤,發現最終會來到AuthorizingRealm的getAuthorizationInfo方法中,在該方法中,首先會去緩存中檢查數據,若是緩存中有數據,則不會執行doGetAuthorizationInfo方法(數據庫操做就在doGetAuthorizationInfo方法中進行),若是緩存中沒有數據,則會執行doGetAuthorizationInfo方法,而且在執行成功後將數據保存到緩存中(前提是配置了緩存,cache不爲null),此時咱們經過斷點,發現執行了緩存而沒有查詢數據庫中的數據,部分源碼以下:
protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) { AuthorizationInfo info = null; Cache<Object, AuthorizationInfo> cache = getAvailableAuthorizationCache(); if (cache != null) { Object key = getAuthorizationCacheKey(principals); info = cache.get(key); } if (info == null) { info = doGetAuthorizationInfo(principals); if (info != null && cache != null) { Object key = getAuthorizationCacheKey(principals); cache.put(key, info); } } return info; }
OK,總體來講shiro中的緩存配置仍是很是簡單的。
That's all.
本小節案例下載地址:https://github.com/lenve/shir...
待續。。。