Shiro從入門到實戰:入門篇

前言

Shiro框架作爲Java安全框架,目前是非常火的,目前市面上比較火的Java安全框架就屬於ShiroSpringSecurity。雖然它的功能沒有Spring Security那麼強大,但是Shiro的API簡單,易於使用。而且相比使用原生的RBAC模型實現認證和授權功能,使用Shiro可以構建更強大的認證授權系統。然後打算寫幾篇博客記錄以及分享Shiro體系知識,達到從認識Shiro到實戰的境界。

  1. Shiro從入門到實戰:入門篇
  2. Shiro從入門到實戰:實戰篇
  3. Shiro從入門到實戰:進階篇
  4. Shiro從入門到實戰:完結篇

1. 學會shiro系列:入門篇

  1. Shiro簡介
  2. Shiro功能介紹
  3. Shiro外部架構
  4. Shiro內部架構
  5. Shiro身份認證(登錄)
  6. Shiro登錄流程源碼分析(重難點)
  7. 什麼是授權
  8. Shiro授權

Shiro簡介

  1. Apache Shiro是一個強大且易用的Java安全框架。Shiro可以非常容易的開發出足夠好的認證授權應用,其不僅可以用在JavaSE環境,也可以用在JavaEE 環境。
  2. Shiro可以幫助我們完成:認證、授權、加密、會話管理、與Web集成、緩存等。而且Shiro的API也是非常簡單。
  3. Shiro不會去維護用戶、維護權限;這些需要我們自己去設計/提供;然後通過相應的接口注入給Shiro即可。

Shiro功能介紹

在這裏插入圖片描述

  1. Authentication:身份認證/登錄,驗證用戶是不是擁有相應的身份;
  2. Authorization:授權,即權限驗證,驗證某個已認證的用戶是否擁有某個權限;即判斷用戶是否能做事情,常見的如:驗證某個用戶是否擁有某個角色。或者細粒度的驗證某個用戶對某個資源是否具有某個權限;
  3. Session Manager:會話管理,即用戶登錄後就是一次會話,在沒有退出之前,它的所有信息都在會話中;會話可以是普通JavaSE環境的,也可以是如Web環境的;
  4. Cryptography:加密,保護數據的安全性,如密碼加密存儲到數據庫,而不是明文存儲;
  5. Web Support:Web支持,可以非常容易的集成到Web環境;
  6. Caching:緩存,比如用戶登錄後,其用戶信息、擁有的角色/權限不必每次去查,這樣可以提高效率;
  7. Concurrency:shiro支持多線程應用的併發驗證,即如在一個線程中開啓另一個線程,能把權限自動傳播過去;
  8. Testing:提供測試支持;
  9. Run As:允許一個用戶假裝爲另一個用戶(如果他們允許)的身份進行訪問;
  10. Remember Me:記住我,這個是非常常見的功能,即一次登錄後,下次再來的話不用登錄了。

Shiro外部架構

在這裏插入圖片描述

  1. Subject:應用代碼直接交互的對象是Subject,也就是說Shiro的對外API核心就是Subject。Subject代表了當前“用戶”, 這個用戶不一定 是一個具體的人,與當前應用交互的任何東西都是Subject,如網絡爬蟲,機器人等;與Subject的所有交互都會委託給 SecurityManager; Subject其實是一個門面,SecurityManager纔是實際的執行者;
  2. SecurityManager:安全管理器;即所有與安全有關的操作都會與SecurityManager交互;且其管理着所有Subject;可以看出它是Shiro的核心,它負責與Shiro的其他組件進行交互,它相當於SpringMVC中DispatcherServlet的角色。
  3. Realm:Shiro 從 Realm 獲取安全數據(如用戶、角色、權限),就是說 SecurityManager 要驗證用戶身份,那麼它需要從 Realm 獲取相應的用戶 進行比較以確定用戶身份是否合法;也需要從 Realm 得到用戶相應的角色/ 權限進行驗證用戶是否能進行操作;可以把 Realm 看成 DataSource。

Shiro內部架構

在這裏插入圖片描述

  1. Subject:主體,可以看到主體可以是任何可以與應用交互的“用戶”。
  2. SecurityManager:相當於SpringMVC中的DispatcherServlet或者Struts2中的FilterDispatcher,是Shiro的心臟;所有具體的交互都通過SecurityManager進行控制;它管理着所有Subject、且負責進行認證和授權、及會話、緩存的管理。
  3. Authenticator:認證器,負責主體認證的,這是一個擴展點,如果用戶覺得Shiro默認的不好,可以自定義實現;其需要認證策略(Authentication Strategy),即什麼情況下算用戶認證通過了。
  4. Authrizer:授權器,或者訪問控制器,用來決定主體是否有權限進行相應的操作;即控制着用戶能訪問應用中的哪些功能。
  5. Realm:可以有1個或多個Realm,可以認爲是安全實體數據源,即用於獲取安全實體的;可以是JDBC實現,也可以是LDAP實現,或者內存實現等等;由用戶提供;注意:Shiro不知道你的用戶/權限存儲在哪及以何種格式存儲;所以我們一般在應用中都需要實現自己的Realm。
  6. SessionManager:如果寫過Servlet就應該知道Session的概念,Session呢需要有人去管理它的生命週期,這個組件就是SessionManager;而Shiro並不僅僅可以用在Web環境,也可以用在如普通的JavaSE環境、EJB等環境;所以呢,Shiro就抽象了一個自己的Session來管理主體與應用之間交互的數據;這樣的話,比如我們在Web環境用,剛開始是一臺Web服務器;接着又上了臺EJB服務器;這時想把兩臺服務器的會話數據放到一個地方,這個時候就可以實現自己的分佈式會話(如把數據放到Memcached服務器)。
  7. SessionDAO:DAO大家都用過,數據訪問對象,用於會話的CRUD,比如我們想把Session保存到數據庫,那麼可以實現自己的SessionDAO,通過如JDBC寫到數據庫;比如想把Session放到Memcached中,可以實現自己的Memcached SessionDAO;另外SessionDAO中可以使用Cache進行緩存,以提高性能。
  8. CacheManager:緩存控制器,來管理如用戶、角色、權限等的緩存的;因爲這些數據基本上很少去改變,放到緩存中後可以提高訪問的性能。
  9. Cryptography:密碼模塊,Shiro提高了一些常見的加密組件用於如密碼加密/解密的。

Shiro身份認證

  1. 身份認證就是確認該用戶(正在登錄的用戶)是否是應用的合法用戶。一般提供身份證,用戶名/密碼來證明。
  2. 在shiro中,用戶需要提供principals (身份)和credentials(憑證)給shiro,以此來認證用戶身份。principals:身份,即主體的標識屬性,可以是任何東西,如用戶名、郵箱等,唯一即可。一個主體可以有多個principals,但只有一個Primary principals,一般是用戶名/密碼/手機號。credentials:憑證,即只有主體知道的安全值,如密碼/數字證書等。

最常見的principalscredentials組合就是用戶名/密碼了。接下來先進行一個基本的身份認證。

  1. 環境搭建,使用Maven構建項目,引入以下依賴:
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>commons-logging</groupId>
        <artifactId>commons-logging</artifactId>
        <version>1.1.3</version>
    </dependency>
    <!-- shiro框架依賴 -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>1.2.2</version>
    </dependency>
    
  2. 我們在類路徑下新建shiro.ini文件,.該文件也可以作爲用戶、角色、權限的提供者,也就是數據源,這裏先提供兩個用戶信息:zwq=22、zhangsan=18。當JavaWeb集成Shiro之後,數據源就變爲數據庫(MySQL,Oracle等等)。
    [users]
    zwq=22
    zhangsan=18
    
  3. 測試代碼, 實現登錄與退出。
    public class LoginLogout {
        public static void main(String[] args) {
            //1.根據.ini配置文件獲取SecurityManagerFactory
            IniSecurityManagerFactory factory =
                    new IniSecurityManagerFactory("classpath:shiro.ini");
            //2.使用工廠獲取SecurityManager示例
            SecurityManager securityManager = factory.getInstance();
            SecurityUtils.setSecurityManager(securityManager);
            //3.獲取主體
            Subject subject = SecurityUtils.getSubject();
            //4.封裝token
            UsernamePasswordToken token = new UsernamePasswordToken("zwq", "22");
            //5.執行認證邏輯:登錄
            subject.login(token);
            //6.判斷是否認證成功
            System.out.println("用戶是否認證:"+subject.isAuthenticated());
            //7.退出
            subject.logout();
            //8.再次判斷是否認證成功
            System.out.println("用戶是否認證:"+subject.isAuthenticated());
        }
    }
    
  4. 在上面代碼中,我們手動封裝了token令牌(用戶名與密碼),然後主體subject攜帶令牌進行認證(subject.login()),底層是委託SecurityManager進行認證(SecurityManager.login()),認證是否成功可以調用主體的isAuthenticated()方法判斷,主體進行退出操作時(subject.logout()),底層也是委託SecurityManager執行退出操作(SecurityManager.logout())。
  5. 上面的用戶名和密碼都是正確的,接下來測試兩種情況:
    一是用戶名正確而密碼錯誤,這種情況會出現IncorrectCredentialsException異常,所以以後出現這個異常就知道是密碼錯了。
    //用戶名正確而命名錯誤
    UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "22");
    
    org.apache.shiro.authc.IncorrectCredentialsException: 
    	Submitted credentials for token [org.apache.shiro.authc.UsernamePasswordToken - zhangsan, rememberMe=false] did not match the expected credentials.
    
    二是用戶名錯誤而密碼正確,這種情況會出現UnknownAccountException異常,所以以後出現這個異常就知道是用戶名錯了。
    //用戶名錯誤而密碼正確
    UsernamePasswordToken token = new UsernamePasswordToken("zs", "18");
    
    org.apache.shiro.authc.UnknownAccountException: 
    	Realm [org.apache.shiro.realm.text.IniRealm@7006c658] was unable to find account data for the submitted AuthenticationToken 
    	[org.apache.shiro.authc.UsernamePasswordToken - zs, rememberMe=false].
    
  6. 列舉常見的異常及其產生原因。
    在這裏插入圖片描述

Shiro登錄流程源碼分析(重難點)

上面我們學會了使用Shiro編寫認證代碼,接下來打個斷點查看並分析認證源碼:

  1. 我們在登錄操作時打斷點,debug運行,程序就停止在斷點處,按下F7進入方法內部
    在這裏插入圖片描述
  2. 我們進入Subject主體的實現類中,subject將登錄操作委託給了SecurityManager,參數除了頁面傳入的token以外還有subject主體,按下F7進入方法內部
    在這裏插入圖片描述
  3. 進入到SecurityManager實現類中,開始執行認證方法,按下F7進入方法內部
    在這裏插入圖片描述
  4. 我們發現SecurityManager將認證任務委託給了認證器,由認證器執行認證方法,按下F7進入方法內部
    在這裏插入圖片描述
  5. 開始執行認證方法,按下F7進入方法內部
    在這裏插入圖片描述
  6. 我們看到方法裏面有一個realms屬性,它是幹啥的?下面解釋Realm是什麼:
    在這裏插入圖片描述
  7. Realm充當了Shiro與應用安全數據間的“橋樑”或者“連接器”。也就是說,當對用戶執行認證(登錄)和授權(訪問控制)驗證時,Shiro會從應用配置的Realm中查找用戶及其權限信息。
  8. 上面使用.ini文件存儲用戶相關信息,以及使用new IniSecurityManagerFactory(“classpath:shiro.ini”)來讀取,那麼Shiro默認會使用它內部定義好的Realm:IniRealm,查看Realm接口的繼承樹也可得知:
    在這裏插入圖片描述
  9. 回到6步驟,因爲只查出一個Realm,所以執行下面方法,按下F7進入方法內部
    在這裏插入圖片描述
  10. 按下F7進入方法內部
    在這裏插入圖片描述
  11. 按下F7進入方法內部
    在這裏插入圖片描述
  12. account是AuthenticationInfo接口的實現類,該方法只是返回token攜帶的用戶名。該方法執行完畢,返回到上圖方法調用處。
    在這裏插入圖片描述
  13. Shiro認證過程是先判斷用戶名是否正確,然後再去匹配密碼,按下F7進入方法內部
    在這裏插入圖片描述
  14. 執行下面方法判斷密碼是否正確,按下F7進入方法內部在這裏插入圖片描述
  15. 這個方法就是把我們剛纔取得的那個realm中的密碼和我們頁面傳入的密碼比較,如果密碼正確就認證成功,如果不正確就拋異常。
    在這裏插入圖片描述
  16. 上面我們分析了Shiro認證的源碼。首先根據用戶名判斷賬號存不存在,存在就去判斷密碼,不存在就拋異常。如果密碼正確,則認證通過,如果不正確,也會拋異常。下圖是Shiro官方給出的認證流程圖,大致和上面分析的差不多。
    在這裏插入圖片描述

什麼是授權

授權,也叫訪問控制,即在應用中控制誰能訪問哪些資源(如訪問頁面/編輯數據/頁面操作等)。
不管是使用Shiro框架,還是其他安全框架,都會涉及這幾個關鍵對象:主體、角色、資源、權限。

  1. 主體,即訪問應用的用戶,在Shiro中使用Subject代表該用戶。用戶只有授權後才允許訪問相應的資源。
  2. 在應用中用戶可以訪問的任何東西,比如訪問頁面、查看/編輯某些數據、訪問某個業務方法、導入導出Excel等等都是資源。用戶只有授權後才能訪問。
  3. 安全策略中的原子授權單位,權限表示在應用中用戶有沒有操作某個資源的權力。即權限表示在應用中用戶能不能訪問某個資源,如:
    訪問用戶列表頁面,查看/新增/修改/刪除用戶數據(即很多時候都是CRUD(增查改刪)式權限控制),導入導出Excel等等。
  4. 角色代表了操作集合,可以理解爲權限的集合,一般情況下我們會賦予用戶角色而不是權限,即這樣用戶可以擁有一組權限,賦予權限時比較方便。典型的如:項目經理、技術總監、CTO、開發工程師等都是角色,不同的角色擁有一組不同的權限。

Shiro支持三種方式的授權

  1. 編程式:通過寫if/else授權代碼塊完成:
    Subject subject = SecurityUtils.getSubject();  
    if(subject.hasRole(“admin”)) {  
        //有權限  
    } else {  
        //無權限  
    } 
    
  2. 註解式:通過在執行的Java方法上放置相應的註解完成:
    @RequiresRoles("admin")  
    public void deleteUser() {  
        //有admin權限才執行  
    } 
    
  3. JSP/thymeleaf標籤:在JSP/thymeleaf頁面通過相應的標籤完成:
    <shiro:hasRole name="admin">  
    <!— 有權限才解析這段HTML代碼 >  
    </shiro:hasRole>   
    

Shiro授權

授權只能在認證成功之後才執行,就像上面說的,如果認證不通過就會報異常,就不可能再執行授權代碼了。而且在我們經常做的Web項目中,用戶輸入用戶名和密碼進行登錄(這就是認證過程),只有成功登錄之後才能進入系統,登錄不成功還是會返回登錄頁面。
接下來在JavaSE環境下使用Shiro編寫代碼判斷用戶是否擁有某些角色。

  1. 還是和上面認證一樣,在類路徑下創建users-roles.ini文件,在其中創建兩個用戶以及每個用戶擁有的角色。
    用戶名=密碼,角色1,角色2,...
    [users]
    zwq=22,admin,tester
    zhangsan=18,tester
    
  2. 測試代碼,判斷用戶擁有的角色。下面把認證邏輯代碼抽離出來,每次執行授權之前進行認證即可。
    boolean hasAllRoles(Collection roleIdentifiers):判斷用戶是否擁有一組權限。是,返回true;不是,返回false。
    boolean hasRole(String roleIdentifier):判斷用戶是否擁有某個角色。
    boolean[] hasRoles(List roleIdentifiers):該方法傳入一組角色集合,逐個判斷是否擁有該角色,有就是true,沒有就是false。
    public class LoginLogout {
    
        private static Subject subject;
    
        public static void main(String[] args) {
        	//認證
            login("classpath:users-roles.ini","zhangsan","18");
            //判斷用戶是否擁有一組權限。是,返回true;不是,返回false。
            System.out.println(subject.hasAllRoles(Arrays.asList("admin", "tester")));
            //判斷用戶是否擁有某個權限
            System.out.println(subject.hasRole("tester"));
            //
            boolean[] hasRoles = subject.hasRoles(Arrays.asList("admin", "tester", "boss"));
            for (int i = 0; i < hasRoles.length; i++) {
                System.out.println(hasRoles[i]);
            }
        }
    	
    	/**
    		封裝認證邏輯代碼
    	*/
        public static void login(String configFile,String username,String password){
            //1.根據.ini配置文件獲取SecurityManagerFactory
            IniSecurityManagerFactory factory =
                    new IniSecurityManagerFactory(configFile);
            //2.使用工廠獲取SecurityManager示例
            SecurityManager securityManager = factory.getInstance();
            SecurityUtils.setSecurityManager(securityManager);
            //3.獲取主體
            subject = SecurityUtils.getSubject();
            //4.封裝token
            UsernamePasswordToken token = new UsernamePasswordToken(username, password);
            //5.執行認證邏輯:登錄
            subject.login(token);
        }
    }
    
  3. 下面再介紹一組判斷角色的方法,這些方法和上面方法的作用一模一樣,只不過沒有返回值,而且在判斷是假的時候就把拋異常。
    void checkRole(String roleIdentifier) throws AuthorizationException;
    void checkRoles(Collection roleIdentifiers) throws AuthorizationException;
    void checkRoles(Collection roleIdentifiers) throws AuthorizationException;
    public class LoginLogout {
    		
        private static Subject subject;
    
        public static void main(String[] args) {
        	//認證
            login("classpath:users-roles.ini","zhangsan","18");
            subject.checkRole("admin");
            subject.checkRoles(Arrays.asList("admin", "tester"));
            subject.checkRoles(Arrays.asList("admin", "tester", "boss"));
        }
    	
    	/**
    		封裝認證邏輯代碼
    	*/
        public static void login(String configFile,String username,String password){
            //1.根據.ini配置文件獲取SecurityManagerFactory
            IniSecurityManagerFactory factory =
                    new IniSecurityManagerFactory(configFile);
            //2.使用工廠獲取SecurityManager示例
            SecurityManager securityManager = factory.getInstance();
            SecurityUtils.setSecurityManager(securityManager);
            //3.獲取主體
            subject = SecurityUtils.getSubject();
            //4.封裝token
            UsernamePasswordToken token = new UsernamePasswordToken(username, password);
            //5.執行認證邏輯:登錄
            subject.login(token);
        }
    }
    

  1. 上面的校驗是基於角色的,是粗粒度的權限校驗。接下來介紹細粒度的權限校驗:基於權限/資源的校驗。重新在類路徑下創建users-roles-permissions.ini文件,編寫規則:“用戶名=密碼,角色1,角色2”“角色=權限1,權限2”。
    [users]
    zwq=22,admin,tester
    zhangsan=18,tester
    [roles]
    admin=user:insert,user:select,user:update
    tester=user:select,user:delete
    
  2. 測試代碼,判斷是否擁有權限有如下三個方法:
    boolean isPermitted(String permission):判斷是否擁有某個權限。
    boolean isPermittedAll(String… permissions):判斷是否擁有一組權限。
    **boolean[] isPermitted(String… permissions)**逐個判斷每一個權限,擁有該權限就爲true,沒有就爲false。
    public class LoginLogout {
    
        private static Subject subject;
    
        public static void main(String[] args) {
            login("classpath:users-roles-permissions.ini","zhangsan","18");
            //判斷擁有某個權限
            System.out.println(subject.isPermitted("user:delete"));
            //判斷擁有一組權限
            System.out.println(subject.isPermittedAll("user:insert", "user:create"));
            boolean[] permitted = subject.isPermitted("user:insert", "user:create");
            for (int i = 0; i < permitted.length; i++) {
                System.out.println(permitted[i]);
            }
        }
    
        public static void login(String configFile,String username,String password){
            //1.根據.ini配置文件獲取SecurityManagerFactory
            IniSecurityManagerFactory factory =
                    new IniSecurityManagerFactory(configFile);
            //2.使用工廠獲取SecurityManager示例
            SecurityManager securityManager = factory.getInstance();
            SecurityUtils.setSecurityManager(securityManager);
            //3.獲取主體
            subject = SecurityUtils.getSubject();
            //4.封裝token
            UsernamePasswordToken token = new UsernamePasswordToken(username, password);
            //5.執行認證邏輯:登錄
            subject.login(token);
        }
    }
    
  3. 再介紹一組校驗權限的方法,和上面一組方法作用一樣,只不過當校驗爲false的時候,會拋出異常。
    void checkPermission(String permission) throws AuthorizationException;
    void checkPermissions(String… permissions) throws AuthorizationException;
    public class LoginLogout {
    
        private static Subject subject;
    
        public static void main(String[] args) {
            login("classpath:users-roles-permissions.ini","zhangsan","18");
            //判斷擁有某個權限
            subject.checkPermission("user:insert");
            //判斷擁有一組權限
            subject.checkPermissions("user:insert", "user:create");
        }
    
        public static void login(String configFile,String username,String password){
            //1.根據.ini配置文件獲取SecurityManagerFactory
            IniSecurityManagerFactory factory =
                    new IniSecurityManagerFactory(configFile);
            //2.使用工廠獲取SecurityManager示例
            SecurityManager securityManager = factory.getInstance();
            SecurityUtils.setSecurityManager(securityManager);
            //3.獲取主體
            subject = SecurityUtils.getSubject();
            //4.封裝token
            UsernamePasswordToken token = new UsernamePasswordToken(username, password);
            //5.執行認證邏輯:登錄
            subject.login(token);
        }
    }
    

總結

  1. 這篇博文只是對Shiro的入門,沒有結合JavaWeb來講解,讓人感覺寫的很簡單的樣子,之後的幾篇會使用Spring,SpringBoot來整合Shiro。如果哪裏寫的不好,也歡迎大家不吝指出。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章