Shiro框架(原理分析與簡單實現)html
有興趣的同窗也能夠閱讀我以前分享的:Java權限管理(受權與認證)CRM權限管理 (PS : 這篇博客裏面的實現方式沒有使用框架,徹底是手寫的受權與認證,能夠幫助理解Shiro框架)前端
http://www.javashuo.com/article/p-oopmnwnq-du.html java
若是發現分享的內容有不合理或者的不對地方,請留言,我會及時定位分析,感謝!!!redis
基本上涉及到用戶參與的系統都要進行權限管理,權限管理屬於系統安全的範疇,權限管理實現對用戶訪問系統的控制,按照安全規則或者安全策略控制用戶能夠訪問並且只能訪問本身被受權的資源。算法
權限管理包括用戶身份認證和受權兩部分,簡稱認證受權。對於須要訪問控制的資源用戶首先通過身份認證,認證經過後用戶具備該資源的訪問權限方可訪問。數據庫
身份認證,就是判斷一個用戶是否爲合法用戶的處理過程.最經常使用的簡單身份認證方式是系統經過覈對用戶輸入的用戶名和口令,看其是否與系統中存儲的該用戶的用戶名和口令一致,來判斷用戶身份是否正確(登陸).對於採用指紋等系統,則出示指紋;對於硬件Key等刷卡系統,則須要刷卡.apache
上邊的流程圖中須要理解如下關鍵對象:編程
Subject:主體 (user)(抽象的概念:保存登陸後的相關信息)數組
訪問系統的用戶,主體能夠是用戶、程序等,進行認證的都稱爲主體;緩存
Principal:身份信息 (username)
是主體(subject)進行身份認證的標識,標識必須具備惟一性,如用戶名、手機號、郵箱地址等,一個主體能夠有多個身份,可是必須有一個主身份(Primary Principal)(相似於數據庫表中的id,保持惟一)。
credential:憑證信息 (password)
是隻有主體本身知道的安全信息,如密碼、證書等。
匿名訪問:相似於淘寶首頁
是否定證經過:淘寶下單,檢查是否登陸,沒有登陸請登陸後下單
受權,即訪問控制,控制誰能訪問哪些資源。主體進行身份認證後須要分配權限方可訪問系統的資源,對於某些資源沒有權限是沒法訪問的。(前提:必須認證經過)
例如:假如你恰好看中了一個高清無碼的種子,而你準備要下載了,這個時候會彈出一個很是噁心的框,讓你登陸才行,好,你辛辛苦苦註冊登陸後,點擊下載,這時有彈出一個框,告訴你必須是VIP用戶才能下載,這時候就是分配權限啓最用,這時你就會去充值一個VIP,而後能夠點擊下載,這時他們就會檢查你這個用戶是否有權限,有權限下載,沒有權限不容許下載。
Apache Shiro是Java的一個安全框架。目前,使用的人愈來愈多,由於它至關簡單,對比Spri9ng Security,功能就沒那麼強大,可是在實際工做時可能並不須要那麼複雜的東西,因此使用小而簡單的Shiro就足夠了。這裏咱們就不糾結它倆哪一個好哪一個壞,能更簡單的解決項目問題就行了。
Apache Shiro是Java的一個安全框架。幫助咱們完成:認證、受權、加密、會話管理、與Web集成、緩存等。
Shiro能夠很是容易的開發出足夠好的應用,其不只能夠用在JavaSE環境,也能夠用在JavaEE環境。Shiro能夠幫助咱們完成:認證、受權、加密、會話管理、與Web集成、緩存等。

Authentication:身份認證/登陸,驗證用戶是否是擁有相應的身份;
Authorization:受權,即權限驗證,驗證某個已認證的用戶是否擁有某個權限;即判斷用戶是否能作事情,常見的如:驗證某個用戶是否擁有某個角色。或者細粒度的驗證某個用戶對某個資源是否具備某個權限;
Session Manager:會話管理,即用戶登陸後就是一次會話,在沒有退出以前,它的全部信息都在會話中;會話能夠是普通JavaSE環境的,也能夠是如Web環境的;
Cryptography:加密,保護數據的安全性,如密碼加密存儲到數據庫,而不是明文存儲;
Web Support:Web支持,能夠很是容易的集成到Web環境;
Caching:緩存,好比用戶登陸後,其用戶信息、擁有的角色/權限沒必要每次去查,這樣能夠提升效率;
Concurrency:shiro支持多線程應用的併發驗證,即如在一個線程中開啓另外一個線程,能把權限自動傳播過去;
Testing:提供測試支持;
Run As:容許一個用戶僞裝爲另外一個用戶(若是他們容許)的身份進行訪問;
Remember Me:記住我,這個是很是常見的功能,即一次登陸後,下次再來的話不用登陸了。
Shiro架構有三個主要概念 - Subject,SecurityManager,Realms。
下圖僅供臨時理解
Subject :訪問系統的用戶,主體能夠是用戶、程序等,進行認證的都稱爲主體;
Subject一詞是一個安全術語,其基本意思是「當前的操做用戶」。它是一個抽象的概念,能夠是人,也能夠是第三方進程或其餘相似事物,如爬蟲,機器人等。
在程序任意位置:Subject currentUser = SecurityUtils.getSubject(); 獲取shiro
一旦得到Subject,你就能夠當即得到你但願用Shiro爲當前用戶作的90%的事情,如登陸、登出、訪問會話、執行受權檢查等
SecurityManager
安全管理器,它是shiro功能實現的核心,負責與後邊介紹的其餘組件(認證器/受權器/緩存控制器)進行交互,實現subject委託的各類功能。有點相似於spirngmvc中的DispatcherServlet前端控制器。
Realms
Realm充當了Shiro與應用安全數據間的「橋樑」或者「鏈接器」。;能夠把Realm當作DataSource,即安全數據源。執行認證(登陸)和受權(訪問控制)時,Shiro會從應用配置的Realm中查找相關的比對數據。以確認用戶是否合法,操做是否合理
在考慮應用安全時,你最常問的問題多是「當前用戶是誰?」或「當前用戶容許作X嗎?」。當咱們寫代碼或設計用戶界面時,問本身這些問題很日常:應用一般都是基於用戶故事構建的,而且你但願功能描述(和安全)是基於每一個用戶的。因此,對於咱們而言,考慮應用安全的最天然方式就是基於當前用戶。Shiro的API用它的Subject概念從根本上體現了這種思考方式。
Subject一詞是一個安全術語,其基本意思是「當前的操做用戶」。稱之爲「用戶」並不許確,由於「用戶」一詞一般跟人相關。在安全領域,術語「Subject」能夠是人,也能夠是第三方進程或其餘相似事物,如爬蟲,機器人等。它僅僅意味着「當前跟軟件交互的東西」。但考慮到大多數目的和用途,你能夠把它認爲是Shiro的「用戶」概念。在代碼的任何地方,你都能輕易的得到Shiro Subject。
Subject currentUser = SecurityUtils.getSubject();
一旦得到Subject,你就能夠當即得到你但願用Shiro爲當前用戶作的90%的事情,如登陸、登出、訪問會話、執行受權檢查等 - 稍後還會看到更多。這裏的關鍵點是Shiro的API很是直觀,由於它反映了開發者以‘每一個用戶’思考安全控制的天然趨勢。同時,在代碼的任何地方都能很輕鬆地訪問Subject,容許在任何須要的地方進行安全操做。
Subject的「幕後」推手是SecurityManager。Subject表明了當前用戶的安全操做,SecurityManager則管理全部用戶的安全操做。它是Shiro框架的核心,充當「保護傘」,引用了多個內部嵌套安全組件,它們造成了對象圖。可是,一旦SecurityManager及其內部對象圖配置好,它就會退居幕後,應用開發人員幾乎把他們的全部時間都花在Subject API調用上。
那麼,如何設置SecurityManager呢?嗯,這要看應用的環境。例如,Web應用一般會在Web.xml中指定一個Shiro Servlet Filter,這會建立SecurityManager實例,若是你運行的是一個獨立應用,你須要用其餘配置方式,但有不少配置選項。
一個應用幾乎老是隻有一個SecurityManager實例。它實際是應用的Singleton。跟Shiro裏幾乎全部組件同樣,SecurityManager的缺省實現是POJO,並且可用POJO兼容的任何配置機制進行配置 - 普通的Java代碼、Spring XML、YAML、.properties和.ini文件等。基原本講,可以實例化類和調用JavaBean兼容方法的任何配置形式均可使用。
能夠看出它是Shiro的核心,它負責與後邊介紹的其餘組件進行交互,若是學習過SpringMVC,你能夠把它當作DispatcherServlet前端控制器。
Shiro的第三個也是最後一個概念是Realm。Realm充當了Shiro與應用安全數據間的「橋樑」或者「鏈接器」。也就是說,當切實與像用戶賬戶這類安全相關數據進行交互,執行認證(登陸)和受權(訪問控制)時,Shiro會從應用配置的Realm中查找不少內容。
從這個意義上講,Realm實質上是一個安全相關的DAO:它封裝了數據源的鏈接細節,並在須要時將相關數據提供給Shiro。當配置Shiro時,你必須至少指定一個Realm,用於認證和(或)受權。配置多個Realm是能夠的,可是至少須要一個。
Realm 能夠理解爲讀取用戶信息、角色及權限的 DAO,就是說SecurityManager要驗證用戶身份與權限,那麼它須要從Realm獲取相應的信息進行比較以肯定用戶身份是否合法;能夠把Realm當作DataSource,即安全數據源。
最簡單的一個Shiro應用:
一、應用代碼經過Subject來進行認證和受權,而Subject又委託給SecurityManager;
二、咱們須要給Shiro的SecurityManager注入Realm,從而讓SecurityManager能獲得數據庫中的用戶及其權限進行判斷;
簡單理解下圖流程:最上層表明其餘程序,進來後,先進入Security Manager 進行受權或者認證(authenticator authorizer),而受權或者認證的數據是從最底層的DB中查詢出來的,而後放到不一樣的realms中(realms中的這些值用來和程序中傳入的數據(程序中傳入的數據被封裝成subject主題)進行比對。
Subject:主體,能夠看到主體能夠是任何能夠與應用交互的「用戶」;
SecurityManager:至關於SpringMVC中的DispatcherServlet或者Struts2中的FilterDispatcher;是Shiro的心臟;全部具體的交互都經過SecurityManager進行控制;它管理着全部Subject、且負責進行認證和受權、及會話、緩存的管理。
Authenticator:認證器,負責主體認證的,這是一個擴展點,若是用戶以爲Shiro默認的很差,能夠自定義實現;其須要認證策略(Authentication Strategy),即什麼狀況下算用戶認證經過了;
Authorizer:受權器,或者訪問控制器,用來決定主體是否有權限進行相應的操做;即控制着用戶能訪問應用中的哪些功能;
Realm:能夠有1個或多個Realm,能夠認爲是安全實體數據源,即用於獲取安全實體的;能夠是JDBC實現,也能夠是LDAP實現,或者內存實現等等;由用戶提供;注意:Shiro不知道你的用戶/權限存儲在哪及以何種格式存儲;因此咱們通常在應用中都須要實現本身的Realm;
SessionManager:若是寫過Servlet就應該知道Session的概念,Session呢須要有人去管理它的生命週期,這個組件就是SessionManager;而Shiro並不只僅能夠用在Web環境,也能夠用在如普通的JavaSE環境、EJB等環境;全部呢,Shiro就抽象了一個本身的Session來管理主體與應用之間交互的數據;能夠實現分佈式的會話管理;
SessionDAO:DAO你們都用過,數據訪問對象,用於會話的CRUD,好比咱們想把Session保存到數據庫,那麼能夠實現本身的SessionDAO,經過如JDBC寫到數據庫;好比想把Session放到redis中,能夠實現本身的redis SessionDAO;另外SessionDAO中可使用Cache進行緩存,以提升性能;
CacheManager:緩存控制器,來管理如用戶、角色、權限等的緩存的;由於這些數據基本上不多去改變,放到緩存中後能夠提升訪問的性能
Cryptography:密碼模塊,Shiro提升了一些常見的加密組件用於如密碼加密/解密的。
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.1.3</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.2.2</version> </dependency> |
首先準備一些用戶身份/憑據(shiro.ini):
[users] zhangsan=666 lisi=888 |
package com.ssm.test;
import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.config.IniSecurityManagerFactory; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.Factory; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory;
public class ShiroTest {
private final static Logger logger = LoggerFactory.getLogger(ShiroTest.class);
@Test public void testLogin() {
// 一、建立SecurityManager工廠對象:加載配置文件,建立工廠對象 Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//二、建立SecurityManager SecurityManager securityManager = factory.getInstance();
//三、將securityManager設置到運行環境中:目的是讓系統隨時隨地能夠訪問securityManger對象 SecurityUtils.setSecurityManager(securityManager);
//四、建立當前登陸的主體,注意:此時主體沒有通過認證 Subject subject = SecurityUtils.getSubject();
//五、收集主體登陸的身份 / 憑證 (建立token令牌),記錄用戶認證的身份和憑證即帳號和密碼 //參數1:將要登陸的用戶名,參數2:登陸用戶的密碼 // 若是參數1出錯 :拋異常 org.apache.shiro.authc.UnknownAccountException // 若是參數2出錯 :拋異常org.apache.shiro.authc.IncorrectCredentialsException UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "666");
//六、主題登陸 try { logger.info("主體登陸"); subject.login(token); } catch (Exception e) { // 登陸失敗 System.out.println("testLogin exception value : " + e.getMessage()); System.out.println("testLogin exception value e : " + e); }
//七、判斷登陸是否成功 System.out.println("驗證登陸是否成功 :" + subject.isAuthenticated());
//八、登出(註銷) subject.logout(); System.out.println("驗證登陸是否成功 :" + subject.isAuthenticated());
} } |
登陸常見異常 :
若是身份驗證失敗請捕獲AuthenticationException或其子類,常見的如: DisabledAccountException(禁用的賬號)、LockedAccountException(鎖定的賬號)、UnknownAccountException(錯誤的賬號)、ExcessiveAttemptsException(登陸失敗次數過多)、IncorrectCredentialsException (錯誤的憑證)、ExpiredCredentialsException(過時的憑證)等
一、調用subject.login方法進行登陸,其會自動委託給securityManager.login方法進行登陸;
二、securityManager經過Authenticator(認證器)進行認證;
三、Authenticator的實現ModularRealmAuthenticator調用realm從ini配置文件取用戶真實的帳號和密碼,這裏使用的是IniRealm(shiro自帶,至關於數據源);
四、IniRealm先根據token中的帳號去ini中找該帳號,若是找不到則給ModularRealmAuthenticator返回null,若是找到則匹配密碼,匹配密碼成功則認證經過。
五、最後調用Subject.logout進行退出操做。
一、用戶名/密碼硬編碼在ini配置文件,之後須要改爲如數據庫存儲,且密碼須要加密存儲;
|
Realm:域,Shiro從從Realm獲取安全數據(如用戶、角色、權限),就是說SecurityManager要驗證用戶身份,那麼它須要從Realm獲取相應的用戶進行比較以肯定用戶身份是否合法;也須要從Realm獲得用戶相應的角色/權限進行驗證用戶是否能進行操做;能夠把Realm當作DataSource,即安全數據源。如咱們以前的ini配置方式將使用org.apache.shiro.realm.text.IniRealm。
org.apache.shiro.realm.Realm接口以下:
------------------------------------------------------------------------------------------------------------------------------------------------------
String getName(); //返回一個惟一的Realm名字
boolean supports(AuthenticationToken token); //判斷此Realm是否支持此Token
AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException; //根據Token獲取認證信息
Realm體系:看圖.
最基礎的是Realm接口,CachingRealm負責緩存處理,AuthenticationRealm負責認證,AuthorizingRealm負責受權,一般自定義的realm繼承AuthorizingRealm。
1. 自定義realm,繼承AuthorizingRealm 重寫3個方法:getName doGetAuthorizationInfo doGetAuthenticaionInfo
2. 配置ini文件,指定使用自定義realm
3. 加載配置文件shiro-realm.ini,執行登陸操做
package com.ssm.realm;
import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection;
public class MyRealm extends AuthorizingRealm {
// 一個系統可能有不一樣的realm,爲了區分使用 @Override public String getName() { return "MyRealm"; }
// 受權操做 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { // TODO Auto-generated method stub return null; }
// 認證操做 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("token value :" + token);
// 參數token:表示登陸時包裝的UsernamePasswordToken
// 經過用戶名到數據庫中查詢用戶信息,封裝成一個AuthenticationInfo對象返回,方便認證器進行對比 // 獲取token中的用戶名 String username = (String) token.getPrincipal();
// 經過用戶名查詢數據庫,將該用戶對應數據查詢返回:帳號與密碼 // 假設查詢數據庫返回數據時:zhangsan 666 if(!"zhangsan".equals(username)) { return null; } String password = "666";
// info對象表示realm登陸比對信息:參數1:用戶信息(真實登陸中是登陸對象user對象),參數2:密碼,參數3:當前realm名字
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, password, getName()); System.out.println("info value :" + info); return info; } } |
ini配置文件指定自定義Realm實現(shiro-realm.ini)
#聲明一個realm myRealm=com.ssm.realm.MyRealm #指定securityManager的realms實現 securityManager.realms=$myRealm |
@Test public void testLoginByRealm() {
// 一、建立SecurityManager工廠對象:加載配置文件,建立工廠對象 Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-realm.ini");
//二、建立SecurityManager SecurityManager securityManager = factory.getInstance();
//三、將securityManager設置到運行環境中:目的是讓系統隨時隨地能夠訪問securityManger對象 SecurityUtils.setSecurityManager(securityManager);
//四、建立當前登陸的主體,注意:此時主體沒有通過認證 Subject subject = SecurityUtils.getSubject();
//五、收集主體登陸的身份 / 憑證 (建立token令牌),記錄用戶認證的身份和憑證即帳號和密碼 //參數1:將要登陸的用戶名,參數2:登陸用戶的密碼 // 若是參數1出錯 :拋異常 org.apache.shiro.authc.UnknownAccountException // 若是參數2出錯 :拋異常org.apache.shiro.authc.IncorrectCredentialsException UsernamePasswordToken token = new UsernamePasswordToken("zhangsa1n", "666");
//六、主題登陸 try { logger.info("主體登陸"); subject.login(token); } catch (Exception e) { // 登陸失敗 e.printStackTrace(); //System.out.println("testLogin exception value : " + e.getMessage()); //System.out.println("testLogin exception value e : " + e); }
//七、判斷登陸是否成功 System.out.println("驗證登陸是否成功 :" + subject.isAuthenticated());
//八、登出(註銷) subject.logout(); System.out.println("驗證登陸是否成功 :" + subject.isAuthenticated()); } |
在涉及到密碼存儲問題上,應該加密/生成密碼摘要存儲,而不是存儲明文密碼。好比以前的600w csdn帳號泄露對用戶可能形成很大損失,所以應加密/生成不可逆的摘要方式存儲。
散列算法通常用於生成數據的摘要信息,是一種不可逆的算法,通常適合存儲密碼之類的數據,常見的散列算法如MD五、SHA等。通常進行散列時最好提供一個salt(鹽),好比加密密碼「admin」,產生的散列值是「21232f297a57a5a743894a0e4a801fc3」,能夠到一些md5解密網站很容易的經過散列值獲得密碼「admin」,即若是直接對密碼進行散列相對來講破解更容易,此時咱們能夠加一些只有系統知道的干擾數據,如用戶名和ID(即鹽);這樣散列的對象是「密碼+用戶名+ID」,這樣生成的散列值相對來講更難破解。
package com.ssm.test;
import org.apache.shiro.crypto.hash.Md5Hash; import org.junit.Test;
public class MD5Test {
@Test public void testMD5() throws Exception {
String password = "666"; // 密碼:明文
// 加密:md5 Md5Hash md5Hash = new Md5Hash(password);
System.out.println(md5Hash); // fae0b27c451c728867a567e8c1bb4e53
// 加密:md5 + 鹽salt md5Hash = new Md5Hash(password, "zhangsan"); System.out.println(md5Hash); // 2f1f526e25fdefa341c7a302b47dd9df
// 加密:md5 + 鹽salt + 散列次數 md5Hash = new Md5Hash(password, "zhangsan", 3); System.out.println(md5Hash); // cd757bae8bd31da92c6b14c235668091 } } |
實際應用是將鹽和散列後的值存在數據庫中,自動realm從數據庫取出鹽和加密後的值由shiro完成密碼校驗。
1. 自定義加密以後realm,重寫3個方法
2. 配置ini配置文件
3. 加載配置文件,測試
package com.ssm.realm;
import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource;
public class PasswordRealm extends AuthorizingRealm {
@Override public String getName() { return "PasswordRealm"; }
// 受權操做 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { return null; }
// 認證操做 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("token value :" + token);
// 參數token:表示登陸時包裝的UsernamePasswordToken
// 經過用戶名到數據庫中查詢用戶信息,封裝成一個AuthenticationInfo對象返回,方便認證器進行對比 // 獲取token中的用戶名 String username = (String) token.getPrincipal();
// 經過用戶名查詢數據庫,將該用戶對應數據查詢返回:帳號與密碼 // 假設查詢數據庫返回數據時:zhangsan 666 if(!"zhangsan".equals(username)) { return null; }
// 模擬數據庫中保存加密以後密文:666 + 帳號 + 散列次數 String password = "cd757bae8bd31da92c6b14c235668091";
// info對象表示realm登陸比對信息:參數1:用戶信息(真實登陸中是登陸對象user對象),參數2:密碼,參數3:鹽,參數4:當前realm名字
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, password, ByteSource.Util.bytes("zhangsan"), getName()); System.out.println("info value :" + info); return info; } } |
[main] #定義憑證匹配器 credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher #散列算法 credentialsMatcher.hashAlgorithmName=md5 #散列次數 credentialsMatcher.hashIterations=3
#將憑證匹配器設置到realm myRealm=com.ssm.realm.PasswordRealm myRealm.credentialsMatcher=$credentialsMatcher securityManager.realms=$myRealm |
@Test public void testLoginByPasswordRealm() {
// 一、建立SecurityManager工廠對象:加載配置文件,建立工廠對象 Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-cryptography.ini");
//二、建立SecurityManager SecurityManager securityManager = factory.getInstance();
//三、將securityManager設置到運行環境中:目的是讓系統隨時隨地能夠訪問securityManger對象 SecurityUtils.setSecurityManager(securityManager);
//四、建立當前登陸的主體,注意:此時主體沒有通過認證 Subject subject = SecurityUtils.getSubject();
//五、收集主體登陸的身份 / 憑證 (建立token令牌),記錄用戶認證的身份和憑證即帳號和密碼 //參數1:將要登陸的用戶名,參數2:登陸用戶的密碼 // 若是參數1出錯 :拋異常 org.apache.shiro.authc.UnknownAccountException // 若是參數2出錯 :拋異常org.apache.shiro.authc.IncorrectCredentialsException UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "666");
//六、主題登陸 try { logger.info("主體登陸"); subject.login(token); } catch (Exception e) { // 登陸失敗 e.printStackTrace(); //System.out.println("testLogin exception value : " + e.getMessage()); //System.out.println("testLogin exception value e : " + e); }
//七、判斷登陸是否成功 System.out.println("驗證登陸是否成功 :" + subject.isAuthenticated());
//八、登出(註銷) subject.logout(); System.out.println("驗證登陸是否成功 :" + subject.isAuthenticated()); } |
shiro 受權(權限控制)實現是居於RBAC而實現的
誰(user)扮演什麼角色(role), 能夠作什麼事情(permission)
RBAC: 基於角色的權限管理
簡單理解爲:誰扮演什麼角色, 被容許作什麼操做
用戶對象:user: 當前操做用戶
角色對象:role:表示權限操做許可權的集合
權限對象:permission: 資源操做許可權
例子:張三(user) 下載(permission)一個高清無碼的種子(資源), 須要VIP權限(role)
張三--->普通用戶--->受權---->VIP用戶----->下載種子
Shiro支持三種方式的受權
經過寫if/else受權代碼塊完成
Subject subject = SecurityUtils.getSubject();
if(subject.hasRole(「admin」)) {
//有權限
} else {
//無權限
}
經過在執行的Java方法上放置相應的註解完成
@RequiresRoles("admin")
public void hello() {
//有權限
}
在JSP頁面經過相應的標籤完成
<shiro:hasRole name="admin">
<!— 有權限 —>
</shiro:hasRole>
權限表達式定義以下:
在ini文件中用戶、角色、權限的配置規則是:「用戶名=密碼,角色1,角色2...」 「角色=權限1,權限2...」,首先根據用戶名找角色,再根據角色找權限,角色是權限集合。
權限字符串的規則是:「資源標識符:操做:資源實例標識符」,意思是對哪一個資源的哪一個實例具備什麼操做,「:」是資源/操做/實例的分割符,權限字符串也可使用*通配符。
例子:
用戶建立權限:user:create,或user:create:*
用戶修改實例001的權限:user:update:001
用戶實例001的全部權限:user:*:001
通常而已,咱們操做只須要關注前面兩節:
資源:操做 :
*:* : 全部資源的全部操做權限--->admin
[users] #用戶zhang的密碼是123,此用戶具備role1和role2兩個角色 zhangsan=666,role1,role2 lisi=888,role2
[roles] #角色role1對資源user擁有create、update權限 role1=user:create,user:update #角色role2對資源user擁有create、delete權限 role2=user:create,user:delete #角色role3對資源user擁有create權限 role3=user:create |
字符串規則
在ini文件中用戶、角色、權限的配置規則是:「用戶名=密碼,角色1,角色2...」 「角色=權限1,權限2...」,首先根據用戶名找角色,再根據角色找權限,角色是權限集合。
權限字符串的規則是:「資源標識符:操做:資源實例標識符」,意思是對哪一個資源的哪一個實例具備什麼操做,「:」是資源/操做/實例的分割符,權限字符串也可使用*通配符。
例子: 用戶建立權限:user:create,或user:create:* 用戶修改實例001的權限:user:update:001 用戶實例001的全部權限:user:*:001 |
@Test public void testHasRole() throws Exception {
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-permission.ini"); SecurityManager securityManager = factory.getInstance(); SecurityUtils.setSecurityManager(securityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "666"); subject.login(token);
// 進行受權操做時前提:用戶必須經過認證
// 判斷當前用戶是否擁有某個角色 System.out.println(subject.hasRole("role1"));
// 判斷當前用戶是否擁有一些角色:返回true表示所有擁有,false 表示不所有擁有 System.out.println(subject.hasAllRoles(Arrays.asList("role1", "role2")));
// 判斷當前用戶是否擁有一些角色:返回boolean類型數據,true表示擁有某個角色,false表示沒有 System.out.println(Arrays.toString(subject.hasRoles(Arrays.asList("role1", "role2"))));
// 判斷當前用戶是否擁有一些角色:沒有返回值, 若是有角色,不作任何操做,沒有報異常:org.apache.shiro.authz.UnauthorizedException subject.checkRole("role1");
try { // 判斷當前用戶是否擁有一些角色 subject.checkRoles("role1", "role2"); } catch (Exception e) { e.printStackTrace(); } } |
@Test public void testHasRole() throws Exception {
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-permission.ini"); SecurityManager securityManager = factory.getInstance(); SecurityUtils.setSecurityManager(securityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "666"); subject.login(token);
// 進行受權操做時前提:用戶必須經過認證 // 判斷當前用戶是否擁有某個權限,返回true表示擁有指定權限,false表示沒有 System.out.println(subject.isPermitted("user:delete")); // 判斷當前用戶是否擁有一些權限,返回true表示都擁有,返回false表示不全擁有 System.out.println(subject.isPermittedAll("user:list", "user:delete")); // 判斷當前用戶是否擁有一些權限,返回boolean數組,true表示有false表示沒有 System.out.println(Arrays.toString(subject.isPermitted("user:list", "user:delete")));
// 判斷當前用戶是否擁有某個權限,沒有返回值,若是沒有擁有指定權限報異常:org.apache.shiro.authz.UnauthorizedException try { subject.checkPermission("user:crea1te"); } catch (Exception e) { e.printStackTrace(); }
// 判斷當前用戶是否擁有某個角色 //System.out.println(subject.hasRole("role1"));
// 判斷當前用戶是否擁有一些角色:返回true表示所有擁有,false 表示不所有擁有 //System.out.println(subject.hasAllRoles(Arrays.asList("role1", "role2")));
// 判斷當前用戶是否擁有一些角色:返回boolean類型數據,true表示擁有某個角色,false表示沒有 //System.out.println(Arrays.toString(subject.hasRoles(Arrays.asList("role1", "role2"))));
// 判斷當前用戶是否擁有一些角色:沒有返回值, 若是有角色,不作任何操做,沒有報異常:org.apache.shiro.authz.UnauthorizedException subject.checkRole("role1");
/*try { // 判斷當前用戶是否擁有一些角色 subject.checkRoles("role1", "role2"); } catch (Exception e) { e.printStackTrace(); }*/ } |
3.5 自定義Realm完成受權
package com.ssm.realm;
import java.util.ArrayList; import java.util.List;
import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection;
public class PermissionRealm extends AuthorizingRealm {
// 一個系統可能有不一樣的realm,爲了區分使用 @Override public String getName() { return "PermissionRealm"; }
// 受權操做 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 傳入參數:principals : 用戶認證憑證信息: // SimpleAuthenticationInfo:認證方法返回封裝認證信息中第一個參數:用戶信息(username)
// 當前登陸用戶名信息:用戶憑證 String username = (String) principals.getPrimaryPrincipal();
// 模擬查詢數據庫:查詢用戶實現指定的角色,以及用戶權限 List<String> roles = new ArrayList<String>(); // 角色集合
List<String> permission = new ArrayList<String>(); // 權限集合
//假設用戶在數據庫中有role1角色 roles.add("role1");
//假設用戶在數據庫中擁有user:delete權限 //permission.add("user:delete"); permission.add("user:*");
// 返回用戶在數據庫中的權限與角色 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRoles(roles); info.addStringPermissions(permission);
return info; }
// 認證操做 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("token value :" + token);
// 參數token:表示登陸時包裝的UsernamePasswordToken
// 經過用戶名到數據庫中查詢用戶信息,封裝成一個AuthenticationInfo對象返回,方便認證器進行對比 // 獲取token中的用戶名 String username = (String) token.getPrincipal();
// 經過用戶名查詢數據庫,將該用戶對應數據查詢返回:帳號與密碼 // 假設查詢數據庫返回數據時:zhangsan 666 if(!"zhangsan".equals(username)) { return null; } String password = "666";
// info對象表示realm登陸比對信息:參數1:用戶信息(真實登陸中是登陸對象user對象),參數2:密碼,參數3:當前realm名字
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, password, getName()); System.out.println("info value :" + info); return info; } } |
[main] #聲明一個realm myReal=com.ssm.realm.PermissionRealm #指定securityManager的realms實現 securityManager.realms=$myReal |
@Test public void testHasRoleByRealm() throws Exception {
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-permission-realm.ini"); SecurityManager securityManager = factory.getInstance(); SecurityUtils.setSecurityManager(securityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "666"); subject.login(token);
// 進行受權操做時前提:用戶必須經過認證 // 判斷當前用戶是否擁有某個權限,返回true表示擁有指定權限,false表示沒有 System.out.println("000"+subject.isPermitted("user:delete"));
// 判斷當前用戶是否擁有某個角色 System.out.println("111"+subject.hasRole("role1")); } |
@Test public void testHasRole() throws Exception {
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-permission.ini"); SecurityManager securityManager = factory.getInstance(); SecurityUtils.setSecurityManager(securityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "666"); subject.login(token);
// 進行受權操做時前提:用戶必須經過認證 // 判斷當前用戶是否擁有某個權限,返回true表示擁有指定權限,false表示沒有 System.out.println(subject.isPermitted("user:delete")); // 判斷當前用戶是否擁有一些權限,返回true表示都擁有,返回false表示不全擁有 System.out.println(subject.isPermittedAll("user:list", "user:delete")); // 判斷當前用戶是否擁有一些權限,返回boolean數組,true表示有false表示沒有 System.out.println(Arrays.toString(subject.isPermitted("user:list", "user:delete")));
// 判斷當前用戶是否擁有某個權限,沒有返回值,若是沒有擁有指定權限報異常:org.apache.shiro.authz.UnauthorizedException try { subject.checkPermission("user:crea1te"); } catch (Exception e) { e.printStackTrace(); }
// 判斷當前用戶是否擁有某個角色 //System.out.println(subject.hasRole("role1"));
// 判斷當前用戶是否擁有一些角色:返回true表示所有擁有,false 表示不所有擁有 //System.out.println(subject.hasAllRoles(Arrays.asList("role1", "role2")));
// 判斷當前用戶是否擁有一些角色:返回boolean類型數據,true表示擁有某個角色,false表示沒有 //System.out.println(Arrays.toString(subject.hasRoles(Arrays.asList("role1", "role2"))));
// 判斷當前用戶是否擁有一些角色:沒有返回值, 若是有角色,不作任何操做,沒有報異常:org.apache.shiro.authz.UnauthorizedException subject.checkRole("role1");
/*try { // 判斷當前用戶是否擁有一些角色 subject.checkRoles("role1", "role2"); } catch (Exception e) { e.printStackTrace(); }*/ } |
一、首先調用Subject.isPermitted*/hasRole*接口,其會委託給SecurityManager,而SecurityManager接着會委託給Authorizer
二、Authorizer是真正的受權者,若是咱們調用如isPermitted(「user:view」),其首先會經過PermissionResolver把字符串轉換成相應的Permission實例;
三、在進行受權以前,其會調用相應的Realm獲取Subject相應的角色/權限用於匹配傳入的角色/權限;
四、Authorizer會判斷Realm的角色/權限是否和傳入的匹配,若是有多個Realm,會委託給ModularRealmAuthorizer進行循環判斷,若是匹配如isPermitted*/hasRole*會返回true,不然返回false表示受權失敗。