30分鐘學會如何使用Shiro

一、架構

要學習如何使用Shiro必須先從它的架構談起,作為一款安全框架Shiro的設計相當精妙。Shiro的應用不依賴任何容器,它也可以在JavaSE下使用。但是最常用的環境還是JavaEE。下面以用戶登錄為例:

(1)使用用戶的登錄信息創建令牌

UsernamePasswordToken token =newUsernamePasswordToken(username, password);

token可以理解為用戶令牌,登錄的過程被抽象為Shiro驗證令牌是否具有合法身份以及相關權限。

(2)執行登陸動作

SecurityUtils.setSecurityManager(securityManager); // 注入SecurityManager

Subject subject=SecurityUtils.getSubject(); // 獲取Subject單例對象

subject.login(token); // 登陸

Shiro的核心部分是SecurityManager,它負責安全認證與授權。Shiro本身已經實現了所有的細節,用戶可以完全把它當做一個黑盒來使用。SecurityUtils對象,本質上就是一個工廠類似spring中的ApplicationContext。Subject是初學者比較難于理解的對象,很多人以為它可以等同于User,其實不然。Subject中文翻譯:項目,而正確的理解也恰恰如此。它是你目前所設計的需要通過Shiro保護的項目的一個抽象概念。通過令牌(token)與項目(subject)的登陸(login)關系,Shiro保證了項目整體的安全。

(3)判斷用戶

Shiro本身無法知道所持有令牌的用戶是否合法,因為除了項目的設計人員恐怕誰都無法得知。因此Realm是整個框架中為數不多的必須由設計者自行實現的模塊,當然Shiro提供了多種實現的途徑,本文只介紹最常見也最重要的一種實現方式——數據庫查詢。

(4)兩條重要的英文

我在學習Shiro的過程中遇到的第一個障礙就是這兩個對象的英文名稱:AuthorizationInfo,AuthenticationInfo。不用懷疑自己的眼睛,它們確實長的很像,不但長的像,就連意思都十分近似。

在解釋它們前首先必須要描述一下Shiro對于安全用戶的界定:和大多數操作系統一樣。用戶具有角色和權限兩種最基本的屬性。例如,我的Windows登陸名稱是learnhow,它的角色是administrator,而administrator具有所有系統權限。這樣learnhow自然就擁有了所有系統權限。那么其他人需要登錄我的電腦怎么辦,我可以開放一個guest角色,任何無法提供正確用戶名與密碼的未知用戶都可以通過guest來登錄,而系統對于guest角色開放的權限極其有限。

同理,Shiro對用戶的約束也采用了這樣的方式。AuthenticationInfo代表了用戶的角色信息集合,AuthorizationInfo代表了角色的權限信息集合。如此一來,當設計人員對項目中的某一個url路徑設置了只允許某個角色或具有某種權限才可以訪問的控制約束的時候,Shiro就可以通過以上兩個對象來判斷。說到這里,大家可能還比較困惑。先不要著急,繼續往后看就自然會明白了。

二、實現Realm

如何實現Realm是本文的重頭戲,也是比較費事的部分。這里大家會接觸到幾個新鮮的概念:緩存機制、散列算法、加密算法。由于本文不會專門介紹這些概念,所以這里僅僅拋磚引玉的談幾點,能幫助大家更好的理解Shiro即可。

(1)緩存機制

Ehcache是很多Java項目中使用的緩存框架,hibernate就是其中之一。它的本質就是將原本只能存儲在內存中的數據通過算法保存到硬盤上,再根據需求依次取出。你可以把Ehcache理解為一個Map對象,通過put保存對象,再通過get取回對象。

以上是ehcache.xml文件的基礎配置,timeToLiveSeconds為緩存的最大生存時間,timeToIdleSeconds為緩存的最大空閑時間,當eternal為false時ttl和tti才可以生效。更多配置的含義大家可以去網上查詢。

(2)散列算法與加密算法

md5是本文會使用的散列算法,加密算法本文不會涉及。散列和加密本質上都是將一個Object變成一串無意義的字符串,不同點是經過散列的對象無法復原,是一個單向的過程。例如,對密碼的加密通常就是使用散列算法,因此用戶如果忘記密碼只能通過修改而無法獲取原始密碼。但是對于信息的加密則是正規的加密算法,經過加密的信息是可以通過秘鑰解密和還原。

(3)用戶注冊

請注意,雖然我們一直在談論用戶登錄的安全性問題,但是說到用戶登錄首先就是用戶注冊。如何保證用戶注冊的信息不丟失,不泄密也是項目設計的重點。

publicclassPasswordHelper {privateRandomNumberGenerator randomNumberGenerator =newSecureRandomNumberGenerator();privateString algorithmName = "md5";privatefinalinthashIterations = 2;publicvoidencryptPassword(User user) {//User對象包含最基本的字段Username和Passworduser.setSalt(randomNumberGenerator.nextBytes().toHex());//將用戶的注冊密碼經過散列算法替換成一個不可逆的新密碼保存進數據,散列過程使用了鹽String newPassword =newSimpleHash(algorithmName, user.getPassword(),

ByteSource.Util.bytes(user.getCredentialsSalt()), hashIterations).toHex();

user.setPassword(newPassword);

}

}

如果你不清楚什么叫加鹽可以忽略散列的過程,只要明白存儲在數據庫中的密碼是根據戶注冊時填寫的密碼所產生的一個新字符串就可以了。經過散列后的密碼替換用戶注冊時的密碼,然后將User保存進數據庫。剩下的工作就丟給UserService來處理。

那么這樣就帶來了一個新問題,既然散列算法是無法復原的,當用戶登錄的時候使用當初注冊時的密碼,我們又應該如何判斷?答案就是需要對用戶密碼再次以相同的算法散列運算一次,再同數據庫中保存的字符串比較。

(4)匹配

CredentialsMatcher是一個接口,功能就是用來匹配用戶登錄使用的令牌和數據庫中保存的用戶信息是否匹配。當然它的功能不僅如此。本文要介紹的是這個接口的一個實現類:HashedCredentialsMatcher

publicclassRetryLimitHashedCredentialsMatcherextendsHashedCredentialsMatcher {//聲明一個緩存接口,這個接口是Shiro緩存管理的一部分,它的具體實現可以通過外部容器注入privateCachepasswordRetryCache;publicRetryLimitHashedCredentialsMatcher(CacheManager cacheManager) {

passwordRetryCache= cacheManager.getCache("passwordRetryCache");

}

@OverridepublicbooleandoCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {

String username=(String) token.getPrincipal();

AtomicInteger retryCount=passwordRetryCache.get(username);if(retryCount ==null) {

retryCount=newAtomicInteger(0);

passwordRetryCache.put(username, retryCount);

}//自定義一個驗證過程:當用戶連續輸入密碼錯誤5次以上禁止用戶登錄一段時間if(retryCount.incrementAndGet() > 5) {thrownewExcessiveAttemptsException();

}booleanmatch =super.doCredentialsMatch(token, info);if(match) {

passwordRetryCache.remove(username);

}returnmatch;

}

}

可以看到,這個實現里設計人員僅僅是增加了一個不允許連續錯誤登錄的判斷。真正匹配的過程還是交給它的直接父類去完成。連續登錄錯誤的判斷依靠Ehcache緩存來實現。顯然match返回true為匹配成功。

(5)獲取用戶的角色和權限信息

說了這么多才到我們的重點Realm,如果你已經理解了Shiro對于用戶匹配和注冊加密的全過程,真正理解Realm的實現反而比較簡單。我們還得回到上文提及的兩個非常類似的對象AuthorizationInfo和AuthenticationInfo。因為Realm就是提供這兩個對象的地方。

publicclassUserRealmextendsAuthorizingRealm {//用戶對應的角色信息與權限信息都保存在數據庫中,通過UserService獲取數據privateUserService userService =newUserServiceImpl();/*** 提供用戶信息返回權限信息*/@OverrideprotectedAuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

String username=(String) principals.getPrimaryPrincipal();

SimpleAuthorizationInfo authorizationInfo=newSimpleAuthorizationInfo();//根據用戶名查詢當前用戶擁有的角色Set roles =userService.findRoles(username);

Set roleNames =newHashSet();for(Role role : roles) {

roleNames.add(role.getRole());

}//將角色名稱提供給infoauthorizationInfo.setRoles(roleNames);//根據用戶名查詢當前用戶權限Set permissions =userService.findPermissions(username);

Set permissionNames =newHashSet();for(Permission permission : permissions) {

permissionNames.add(permission.getPermission());

}//將權限名稱提供給infoauthorizationInfo.setStringPermissions(permissionNames);returnauthorizationInfo;

}/*** 提供賬戶信息返回認證信息*/@OverrideprotectedAuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)throwsAuthenticationException {

String username=(String) token.getPrincipal();

User user=userService.findByUsername(username);if(user ==null) {//用戶名不存在拋出異常thrownewUnknownAccountException();

}if(user.getLocked() == 0) {//用戶被管理員鎖定拋出異常thrownewLockedAccountException();

}

SimpleAuthenticationInfo authenticationInfo=newSimpleAuthenticationInfo(user.getUsername(),

user.getPassword(), ByteSource.Util.bytes(user.getCredentialsSalt()), getName());returnauthenticationInfo;

}

}

根據Shiro的設計思路,用戶與角色之前的關系為多對多,角色與權限之間的關系也是多對多。在數據庫中需要因此建立5張表,分別是用戶表(存儲用戶名,密碼,鹽等)、角色表(角色名稱,相關描述等)、權限表(權限名稱,相關描述等)、用戶-角色對應中間表(以用戶ID和角色ID作為聯合主鍵)、角色-權限對應中間表(以角色ID和權限ID作為聯合主鍵)。具體dao與service的實現本文不提供??傊Y論就是,Shiro需要根據用戶名和密碼首先判斷登錄的用戶是否合法,然后再對合法用戶授權。而這個過程就是Realm的實現過程。

(6)會話

用戶的一次登錄即為一次會話,Shiro也可以代替Tomcat等容器管理會話。目的是當用戶停留在某個頁面長時間無動作的時候,再次對任何鏈接的訪問都會被重定向到登錄頁面要求重新輸入用戶名和密碼而不需要程序員在Servlet中不停的判斷Session中是否包含User對象。啟用Shiro會話管理的另一個用途是可以針對不同的模塊采取不同的會話處理。以淘寶為例,用戶注冊淘寶以后可以選擇記住用戶名和密碼。之后再次訪問就無需登陸。但是如果你要訪問支付寶或購物車等鏈接依然需要用戶確認身份。當然,Shiro也可以創建使用容器提供的Session最為實現。

三、與SpringMVC集成

有了注冊模塊和Realm模塊的支持,下面就是如何與SpringMVC集成開發。有過框架集成經驗的同學一定知道,所謂的集成基本都是一堆xml文件的配置,Shiro也不例外。

(1)配置前端過濾器

先說一個題外話,Filter是過濾器,interceptor是攔截器。前者基于回調函數實現,必須依靠容器支持。因為需要容器裝配好整條FilterChain并逐個調用。后者基于代理實現,屬于AOP的范疇。

如果希望在WEB環境中使用Shiro必須首先在web.xml文件中配置

Shiro_Project

index.jsp

SpringMVCorg.springframework.web.servlet.DispatcherServletcontextConfigLocationclasspath:springmvc.xml1true

SpringMVC/

org.springframework.web.context.ContextLoaderListener

org.springframework.web.util.Log4jConfigListener

contextConfigLocationclasspath:spring.xml,classpath:spring-shiro-web.xml

log4jConfigLoactionclasspath:log4j.properties

shiroFilterorg.springframework.web.filter.DelegatingFilterProxytruetargetFilterLifecycletrue

shiroFilter/*

熟悉Spring配置的同學可以重點看有綠字注釋的部分,這里是使Shiro生效的關鍵。由于項目通過Spring管理,因此所有的配置原則上都是交給Spring。DelegatingFilterProxy的功能是通知Spring將所有的Filter交給ShiroFilter管理。

接著在classpath路徑下配置spring-shiro-web.xml文件

http://www.springframework.org/schema/beans/spring-beans-3.1.xsd

http://www.springframework.org/schema/context

http://www.springframework.org/schema/context/spring-context-3.1.xsd

http://www.springframework.org/schema/mvc

http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">/authc/admin = roles[admin]

/authc/** = authc

/** = anon

需要注意filterChainDefinitions過濾器中對于路徑的配置是有順序的,當找到匹配的條目之后容器不會再繼續尋找。因此帶有通配符的路徑要放在后面。三條配置的含義是: /authc/admin需要用戶有用admin權限、/authc/**用戶必須登錄才能訪問、/**其他所有路徑任何人都可以訪問。

說了這么多,大家一定關心在Spring中引入Shiro之后到底如何編寫登錄代碼呢。

@ControllerpublicclassLoginController {

@AutowiredprivateUserService userService;

@RequestMapping("login")publicModelAndView login(@RequestParam("username") String username, @RequestParam("password") String password) {

UsernamePasswordToken token=newUsernamePasswordToken(username, password);

Subject subject=SecurityUtils.getSubject();try{

subject.login(token);

}catch(IncorrectCredentialsException ice) {//捕獲密碼錯誤異常ModelAndView mv =newModelAndView("error");

mv.addObject("message", "password error!");returnmv;

}catch(UnknownAccountException uae) {//捕獲未知用戶名異常ModelAndView mv =newModelAndView("error");

mv.addObject("message", "username error!");returnmv;

}catch(ExcessiveAttemptsException eae) {//捕獲錯誤登錄過多的異常ModelAndView mv =newModelAndView("error");

mv.addObject("message", "times error");returnmv;

}

User user=userService.findByUsername(username);

subject.getSession().setAttribute("user", user);returnnewModelAndView("success");

}

}

登錄完成以后,當前用戶信息被保存進Session。這個Session是通過Shiro管理的會話對象,要獲取依然必須通過Shiro。傳統的Session中不存在User對象。

@Controller

@RequestMapping("authc")publicclassAuthcController {///authc/** = authc 任何通過表單登錄的用戶都可以訪問@RequestMapping("anyuser")publicModelAndView anyuser() {

Subject subject=SecurityUtils.getSubject();

User user= (User) subject.getSession().getAttribute("user");

System.out.println(user);returnnewModelAndView("inner");

}///authc/admin = user[admin] 只有具備admin角色的用戶才可以訪問,否則請求將被重定向至登錄界面@RequestMapping("admin")publicModelAndView admin() {

Subject subject=SecurityUtils.getSubject();

User user= (User) subject.getSession().getAttribute("user");

System.out.println(user);returnnewModelAndView("inner");

}

}

四、總結

Shiro是一個功能很齊全的框架,使用起來也很容易,但是要想用好卻有相當難度。完整項目的源碼來源

http://minglisoft.cn/technology歡迎大家一起學習研究相關技術,源碼獲取請加求求(企鵝):2042849237

,需要交流的同學可以給我留言。如果大家感覺我寫的還可以,也希望能給我一些反饋意見。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內容

  • 文章轉載自:http://blog.csdn.net/w1196726224/article/details/53...
    wangzaiplus閱讀 3,408評論 0 3
  • 一.Shiro簡介 Shiro框架是和spring security框架作用差不多的一個安全認證授權框架,但它更加...
    興厚閱讀 5,543評論 0 14
  • 前言 相比有做過企業級開發的童鞋應該都有做過權限安全之類的功能吧,最先開始我采用的是建用戶表,角色表,權限表,之后...
    crossoverJie閱讀 41,027評論 31 179
  • 構建一個互聯網應用,權限校驗管理是很重要的安全措施,這其中主要包含: 認證 - 用戶身份識別,即登錄 授權 - 訪...
    zhuke閱讀 3,526評論 0 30
  • 191 3.07早讀 You can have anything you want. You just need ...
    123逍遙游閱讀 313評論 0 1