1. 程式人生 > 實用技巧 >Shiro 安全框架

Shiro 安全框架

  1. Shiro安全框架簡介

    1. Shiro概述

Shiro是apache旗下一個開源安全框架(http://shiro.apache.org/),它將軟體系統的安全認證相關的功能抽取出來,實現使用者身份認證,許可權授權、加密、會話管理等功能,組成了一個通用的安全認證框架。使用shiro就可以非常快速的完成認證、授權等功能的開發,降低系統成本。

使用者在進行資源訪問時,要求系統要對使用者進行許可權控制,其具體流程如圖-1所示:

  1. Shiro概要架構

在概念層面,Shiro 架構包含三個主要的理念,如圖-2所示:

其中:

  1. Subject :主體物件,負責提交使用者認證和授權資訊。

  2. SecurityManager:安全管理器,負責認證,授權等業務實現。

  3. Realm:領域物件,負責從資料層獲取業務資料。

    1. Shiro詳細架構

    Shiro框架進行許可權管理時,要涉及到的一些核心物件,主要包括:認證管理物件,授權管理物件,會話管理物件,快取管理物件,加密管理物件以及Realm管理物件(領域物件:負責處理認證和授權領域的資料訪問題)等,其具體架構如圖-3所示:

    其中:

    1. Subject(主體):與軟體互動的一個特定的實體(使用者、第三方服務等)。

    2. SecurityManager(安全管理器) :Shiro 的核心,用來協調管理元件工作。

    3. Authenticator(認證管理器):負責執行認證操作。

    4. Authorizer(授權管理器):負責授權檢測。

    5. SessionManager(會話管理):負責建立並管理使用者 Session 生命週期,提供一個強有力的 Session 體驗。

    6. SessionDAO:代表 SessionManager 執行 Session 持久(CRUD)動作,它允許任何儲存的資料掛接到 session 管理基礎上。

    7. CacheManager(快取管理器):提供建立快取例項和管理快取生命週期的功能。

    8. Cryptography(加密管理器):提供了加密方式的設計及管理。

    9. Realms(領域物件):是shiro和你的應用程式安全資料之間的橋樑。

      1. Shiro框架認證攔截實現(filter)

        1. Shiro基本環境配置

          1. 新增shiro依賴

      實用spring整合shiro時,需要在pom.xml中新增如下依賴:

      <dependency>
         <groupId>org.apache.shiro</groupId>
         <artifactId>shiro-spring</artifactId>
         <version>1.5.2</version>
      </dependency>
      1. Shiro核心物件配置

      基於SpringBoot 實現的專案中,沒有提供shiro的自動化配置,需要我們自己配置。

      第一步:建立SpringShiroConfig類。關鍵程式碼如下:

      package com.cy.pj.common.config;
      /**@Configuration 註解描述的類為一個配置物件,
       * 此物件也會交給spring管理
       */
      @Configuration
      public class SpringShiroConfig {
       
      }

      第二步:在Shiro配置類中新增SecurityManager配置(這裡一定要使用org.apache.shiro.mgt.SecurityManager這個介面物件),關鍵程式碼如下:

      @Bean
      public SecurityManager securityManager() {
               DefaultWebSecurityManager sManager=
               new DefaultWebSecurityManager();
               return sManager;
      }

      第三步: 在Shiro配置類中新增ShiroFilterFactoryBean物件的配置。通過此物件設定資源匿名訪問、認證訪問。關鍵程式碼如下

@Bean
public ShiroFilterFactoryBean shiroFilterFactory (
             SecurityManager securityManager,) {
         ShiroFilterFactoryBean sfBean=
         new ShiroFilterFactoryBean();
         sfBean.setSecurityManager(securityManager);
         //定義map指定請求過濾規則(哪些資源允許匿名訪問,哪些必須認證訪問)
         LinkedHashMap<String,String> map= new LinkedHashMap<>();
         //靜態資源允許匿名訪問:"anon"
         map.put("/bower_components/**","anon");
         map.put("/build/**","anon");
         map.put("/dist/**","anon");
         map.put("/plugins/**","anon");
         //除了匿名訪問的資源,其它都要認證("authc")後訪問
         map.put("/**","authc");
         sfBean.setFilterChainDefinitionMap(map);
         return sfBean;
     }

其配置過程中,物件關係如下圖-4所示:

  1. Shiro登陸頁面呈現

    1. 服務端Controller實現

  • 業務描述及設計實現

當服務端攔截到使用者請求以後,判定此請求是否已經被認證,假如沒有認證應該先跳轉到登入頁面。

  • 關鍵程式碼分析及實現.

第一步:在PageController中新增一個呈現登入頁面的方法,關鍵程式碼如下:

@RequestMapping("doLoginUI")
public String doLoginUI(){
        return "login";
}

第二步:修改SpringShiroConfig類中shiroFilterFactorybean的配置,新增登陸url的設定。關鍵程式碼見sfBean.setLoginUrl("/doLoginUI") 第7行 部分。

 1 @Bean
 2 public ShiroFilterFactoryBean shiroFilterFactory (
 3              @Autowired SecurityManager securityManager) {
 4          ShiroFilterFactoryBean sfBean=
 5          new ShiroFilterFactoryBean();
 6          sfBean.setSecurityManager(securityManager);
 7  sfBean.setLoginUrl("/doLoginUI");
 8 //定義map指定請求過濾規則(哪些資源允許匿名訪問,
 9 哪些必須認證訪問)
10          LinkedHashMap<String,String> map=
11                  new LinkedHashMap<>();
12          //靜態資源允許匿名訪問:"anon"
13          map.put("/bower_components/**","anon");
14          map.put("/modules/**","anon");
15          map.put("/dist/**","anon");
16          map.put("/plugins/**","anon");
17          //除了匿名訪問的資源,其它都要認證("authc")後訪問
18          map.put("/**","authc");
19          sfBean.setFilterChainDefinitionMap(map);
20          return sfBean;
21 }
  1. Shiro框架認證業務實現

    1. 認證流程分析

身份認證即判定使用者是否是系統的合法使用者,使用者訪問系統資源時的認證(對使用者身份資訊的認證)流程圖-5所示:

步驟1:應用程式程式碼呼叫該Subject.login方法,並傳入AuthenticationToken表示終端使用者的主體和憑據的構造例項。

第2步Subject例項(通常是一個DelegatingSubject(或子類))SecurityManager通過呼叫來委託應用程式例項securityManager.login(token)從此處開始實際的身份驗證工作。

步驟3SecurityManager作為基本的“傘”元件,接收令牌並Authenticator通過呼叫來簡單地委派給其內部例項authenticator.authenticate(token)這幾乎總是一個ModularRealmAuthenticator例項,它支援Realm在身份驗證期間協調一個或多個例項。ModularRealmAuthenticator本質上提供一個PAM為Apache四郎(其中每個樣式的範例Realm是在PAM術語一個“模組”)。

步驟4:如果Realm為該應用程式配置了多個,則該ModularRealmAuthenticator例項將Realm使用其configureed發起多身份驗證嘗試AuthenticationStrategyRealms呼叫進行身份驗證之前,期間和之後,將呼叫,AuthenticationStrategy以允許它對每個Realm的結果做出反應。我們將AuthenticationStrategies儘快覆蓋

第5步Realm諮詢每個配置,以檢視是否supports已提交AuthenticationToken如果是這樣,支援的RealmgetAuthenticationInfo方法將與Submitted一起呼叫tokengetAuthenticationInfo方法有效地表示針對該特定物件的單個身份驗證嘗試Realm我們將Realm很快介紹身份驗證行為。

簡單說就是

  1. 系統呼叫subject的login方法將使用者資訊提交給SecurityManager

  2. SecurityManager將認證操作委託給認證器物件Authenticator

  3. Authenticator將使用者輸入的身份資訊傳遞給Realm。

  4. Realm訪問資料庫獲取使用者資訊然後對資訊進行封裝並返回。

  5. Authenticator 對realm返回的資訊進行身份認證。


    1. 認證服務端實現

      1. 核心業務分析

    認證業務API處理流程分析,如圖-6所示:

    • 業務描述及設計實現。

    在使用者資料層物件SysUserDao中,按特定條件查詢使用者資訊,並對其進行封裝。

    • 關鍵程式碼分析及實現。

    在SysUserDao介面中,新增根據使用者名稱獲取使用者物件的方法,關鍵程式碼如下:

    SysUser findUserByUserName(String username)。
    1. Mapper元素定義

    • 業務描述及設計實現。

    根據SysUserDao中定義的方法,在SysUserMapper檔案中新增元素定義。

    • 關鍵程式碼分析及實現。

    基於使用者名稱獲取使用者物件的方法,關鍵程式碼如下:

    <select id="findUserByUserName"
               resultType="com.cy.pj.sys.entity.SysUser">
          select *
          from sys_users  
          where username=#{username}
       </select>
    1. Service介面及實現

    • 業務描述及設計實現。

    本模組的業務在Realm型別的物件中進行實現,我們編寫realm時,要繼承

    AuthorizingRealm並重寫相關方法,完成認證及授權業務資料的獲取及封裝。

    • 關鍵程式碼分析及實現。

    第一步:定義ShiroUserRealm類,關鍵程式碼如下:

    package com.cy.pj.sys.service.realm;
    @Service
    public class ShiroUserRealm extends AuthorizingRealm {
     
        @Autowired
        private SysUserDao sysUserDao;
            
        /**
         * 設定憑證匹配器(與使用者新增操作使用相同的加密演算法)
         */
        @Override
        public void setCredentialsMatcher(
              CredentialsMatcher credentialsMatcher) {
            //構建憑證匹配物件 
            HashedCredentialsMatcher cMatcher=
            new HashedCredentialsMatcher();
            //設定加密演算法
            cMatcher.setHashAlgorithmName("MD5");
            //設定加密次數
            cMatcher.setHashIterations(1);
            super.setCredentialsMatcher(cMatcher);
        }
        /**
         * 通過此方法完成認證資料的獲取及封裝,系統
         * 底層會將認證資料傳遞認證管理器,由認證
         * 管理器完成認證操作。
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(
                AuthenticationToken token) 
                throws AuthenticationException {
            //1.獲取使用者名稱(使用者頁面輸入)
            UsernamePasswordToken upToken=
            (UsernamePasswordToken)token;
            String username=upToken.getUsername();
            //2.基於使用者名稱查詢使用者資訊
            SysUser user=
            sysUserDao.findUserByUserName(username);
            //3.判定使用者是否存在
            if(user==null)
            throw new UnknownAccountException();
            //4.判定使用者是否已被禁用。
            if(user.getValid()==0)
            throw new LockedAccountException();
            
            //5.封裝使用者資訊
            ByteSource credentialsSalt=
            ByteSource.Util.bytes(user.getSalt());
            //記住:構建什麼物件要看方法的返回值
            SimpleAuthenticationInfo info=
            new SimpleAuthenticationInfo(
                    user,//principal (身份)
                    user.getPassword(),//hashedCredentials
                    credentialsSalt, //credentialsSalt
                    getName());//realName
            //6.返回封裝結果
            return info;//返回值會傳遞給認證管理器(後續
            //認證管理器會通過此資訊完成認證操作)
        }
        ....
    }

    第二步:對此realm,需要在SpringShiroConfig配置類中,注入給SecurityManager物件,修改securityManager方法,見黃色背景部分,例如:

    1 @Bean
    2 public SecurityManager securityManager(Realm realm) {
    3          DefaultWebSecurityManager sManager=
    4          new DefaultWebSecurityManager();
    5          sManager.setRealm(realm);
    6          return sManager;
    7 }
    1. Controller 類實現

    • 業務描述及設計實現。

    在此物件中定義相關方法,處理客戶端的登陸請求,例如獲取使用者名稱,密碼等然後提交該shiro框架進行認證。

    • 關鍵程式碼分析及實現。

    第一步:在SysUserController中新增處理登陸的方法。關鍵程式碼如下:

     1    @RequestMapping("doLogin")
     2        public JsonResult doLogin(String username,String password){
     3            //1.獲取Subject物件
     4            Subject subject=SecurityUtils.getSubject();
     5            //2.通過Subject提交使用者資訊,交給shiro框架進行認證操作
     6            //2.1對使用者進行封裝
     7            UsernamePasswordToken token=
     8            new UsernamePasswordToken(
     9                    username,//身份資訊
    10                    password);//憑證資訊
    11            //2.2對使用者資訊進行身份認證
    12            subject.login(token);
    13            //分析:
    14            //1)token會傳給shiro的SecurityManager
    15            //2)SecurityManager將token傳遞給認證管理器
    16            //3)認證管理器會將token傳遞給realm
    17            return new JsonResult("login ok");
    18        }

    第二步:修改shiroFilterFactory的配置,對/user/doLogin這個路徑進行匿名訪問的配置,檢視如下黃色標記部分的程式碼:

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactory (
                 SecurityManager securityManager) {
             ShiroFilterFactoryBean sfBean=
             new ShiroFilterFactoryBean();
             sfBean.setSecurityManager(securityManager);
             //假如沒有認證請求先訪問此認證的url
             sfBean.setLoginUrl("/doLoginUI");
             //定義map指定請求過濾規則(哪些資源允許匿名訪問,哪些必須認證訪問)
             LinkedHashMap<String,String> map=
                     new LinkedHashMap<>();
             //靜態資源允許匿名訪問:"anon"
             map.put("/bower_components/**","anon");
             map.put("/build/**","anon");
             map.put("/dist/**","anon");
             map.put("/plugins/**","anon");
     
      map.put("/user/doLogin","anon");                       //authc表示,除了匿名訪問的資源,其它都要認證("authc")後才能訪問訪問
             map.put("/**","authc");
             sfBean.setFilterChainDefinitionMap(map);
             return sfBean;
         }

    第三步:當我們在執行登入操作時,為了提高使用者體驗,可對系統中的異常資訊進行處理,例如,在統一異常處理類中新增如下方法:

    @ExceptionHandler(ShiroException.class) 
       @ResponseBody
        public JsonResult doHandleShiroException(
                ShiroException e) {
            JsonResult r=new JsonResult();
            r.setState(0);
            if(e instanceof UnknownAccountException) {
                r.setMessage("賬戶不存在");
            }else if(e instanceof LockedAccountException) {
                r.setMessage("賬戶已被禁用");
            }else if(e instanceof IncorrectCredentialsException) {
                r.setMessage("密碼不正確");
            }else if(e instanceof AuthorizationException) {
                r.setMessage("沒有此操作許可權");
            }else {
                r.setMessage("系統維護中");
            }
            e.printStackTrace();
            return r;
        }