1. 程式人生 > 實用技巧 >Shiro框架-----用來實現認證和授權的操作

Shiro框架-----用來實現認證和授權的操作

                                      Shiro框架

1.概念:shiro : 是一個許可權管理控制框架。主要提供了認證與授權的操作。

    認證:使用者得登入成功後才能訪問頁面。 (這時是可以訪問所有頁面)

    授權:授權訪問資源,是指使用者認證成功的情況下,再判斷模組(許可權)來訪問 可訪問的頁面。 (這時是隻能訪問又許可權的頁面)


2.shiro 和 spring security的比較

 ①shiro比spring security簡單

 ②shiro需要和spring整合,而spring security不需要

 ③spring security功能更加的強大


3.shiro的核心實現是---過濾器(攔截或過慮請求),用於使用者訪問頁面(URL)時進行許可權控制

4.shiro的主要功能模組有:

  Authentication:身份認證/登入,------核心功能

  Authorization:授權,即許可權驗證,驗證某個已認證的使用者是否擁有某個許可權 --------核心功能

  Session Manager:會話管理,即使用者登入後就是一次會話,在沒有退出之前,它的所有資訊都在會話中

  Cryptography:加密,保護資料的安全性,

  Web Support:Web支援

  Caching:快取

  Concurrency:shiro支援多執行緒應用的併發驗證

記住一點,Shiro不會去維護使用者和維護許可權;這些需要我們自己去設計/提供;然後通過相應的介面注入給Shiro即可


5.shiro的三大元件

  Subject:主體,代表了當前“使用者”,這個使用者不一定是一個具體的人,與當前應用互動的任何東西都是Subject,

      儲存使用者資訊,負責認證和授權方法呼叫,只要調了方法就會到SecurityManager

  SecurityManager:安全管理器;即所有與安全有關的操作都會與SecurityManager互動;它管理著所有Subject;可以看出它是Shiro的核心,負責認證和授權的管理    

  Realm:域,連線資料庫獲取認證和授權資訊的橋樑,Shiro從Realm獲取安全資料(如使用者、角色、許可權);

也就是說對於我們而言,最簡單的一個Shiro應用:
1、應用程式碼通過Subject來進行認證和授權,而Subject又委託給SecurityManager;
2、我們需要給Shiro的SecurityManager注入Realm,從而讓SecurityManager能得到合法的使用者及其許可權進行判斷。
從以上也可以看出,Shiro不提供維護使用者/許可權,而是通過Realm讓開發人員自己注入。


6.shiro的環境搭建

  專案新增shiro依賴 (父專案已經新增) (shiro通常是一個專案的一部分) 

    <!--shiro-->
    <!--shiro和spring整合-->
    <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-spring</artifactId>
      <version>1.3.2</version>
      </dependency>
    <!--shiro核心包-->
    <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-core</artifactId>
      <version>1.3.2</version>
    </dependency>

  配置web.xml

<!--Spring整合Shiro的過濾器
作用: 接收所有需要交給Shiro過濾的請求

過濾器的名稱: filter-name就是Spring的bean的id
-->
<filter>
  <filter-name>shiroFilter</filter-name>
  <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
  <filter-name>shiroFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

  配置applicationContext-shiro.xml, Spring整合shiro

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<!--1.Shiro處理認證授權邏輯的物件-->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!--安全管理器-->
  <property name="securityManager" ref="securityManager"/>
</bean>

<!--2.配置安全管理器-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!--配置Realm-->
  <property name="realm" ref="myRealm"/>
</bean>

<!--3.配置Realm-->
<bean id="myRealm" class="cn.itcast.web.shiro.AuthRealm"/>

</beans>

  建立自定義realm類:AuthRealm,繼承AuthorizingRealm    

public class AuthRealm extends AuthorizingRealm{
  //授權
  @Override
  protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    return null;
  }

  //認證
  @Override
  protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    return null;
  }
}


7.shiro的使用

前引:

一:與認證(登入)相關的過濾器的使用
過濾器:(先攔截,在判斷)
anon:匿名過濾器,代表不認證(登入)也可以訪問該請求,通過對靜態資源進行放行(指css,js,圖片都沒有了)
authc:認證過濾器,代表必須認證成功才可以訪問該資源,如果認證不成功或沒有認證,會自動跳轉到login.jsp頁面(自帶的),但是可以修改預設登入頁面,用<property name="loginUrl" value="/login.jsp"/>
用法:
/make/**=anon
/** = authc
anon一定要放在authc前,/** = authc表示攔截所有的請求,而在其上面的/make/**=anon表示這個放行不攔截
該攔截的資源用authc,該放行的資源用anon
又由於靜態資源也會被攔截,所以anon主要用來給靜態資源放行(css,js,圖片等)
/login.do因為是登入請求,必須使用anon過濾器放行!!!否則無法登入啦!

二與授權相關的過濾器

第一部分:Shiro登陸認證(一)使用認證過濾器

  第一步:applicationContext-shiro.xml新增認證過濾器

<!--1.Shiro處理認證授權邏輯的物件-->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <!--安全管理器-->
        <property name="securityManager" ref="securityManager"/>


        <!--設定Shiro的過濾器-->

        <!--
          過濾的路徑問題:
            /index.jsp :  準確攔截/index.jsp頁面
            /user/*  : 模糊匹配,攔截user根目錄下的所有資源(代表一級目錄)
            /user/**  : 模糊匹配,攔截user目錄下的所有資源(代表任意級目錄)

          1. 認證相關的過濾器
             anon: 匿名過濾器,不用認證也可以訪問該資源(必須放在authc的前面)
             authc:認證過濾器,必須認證成功才可以訪問該資源,如果認證不成功,會自動跳轉到login.jsp頁面(可以通過loginUrl屬性更新登入頁面)
        -->
        <property name="filterChainDefinitions">
            <value>
                /css/**=anon
                /img/**=anon
                /make/**=anon
                /plugins/**=anon
                /login.do=anon
                /**=authc
            </value>
        </property>

        <!--修改shrio預設登入頁面-->
        <property name="loginUrl" value="/login.jsp"/>

    </bean>

  第二步:修改LoginController,通過shiro實現登陸認證

/**
     * 登入方法
     *  1)URL: http://localhost:8080/login.do
     *  2)引數:email=eric&password=123
     *  3)返回:/WEB-INF/pages/home/main.jsp
     */
    @RequestMapping("/login")
    public String login(String email,String password){
        //1.判斷是否為空
        if(StringUtils.isEmpty(email) || StringUtils.isEmpty(password)){
            request.setAttribute("error","郵箱和密碼不能為空");
            //手動轉發到login.jsp頁面
            return "forward:/login.jsp";
        }
  //-----------------------------------------------------一以下是正常做法--------------------------------------------------------
        /*//2.判斷email是否存在
        User loginUser = userService.findByEmail(email);

        //3 email不存在,提示"使用者名稱不存在"
        if(loginUser==null){
            request.setAttribute("error","使用者名稱不存在");
            //手動轉發到login.jsp頁面
            return "forward:/login.jsp";
        }

        //4 email存在,繼續判斷password是否正確
        if(loginUser.getPassword().equals(password)){
            //5. password正確,登入成功,儲存使用者資料到session域中,跳轉到主頁
            session.setAttribute("loginUser",loginUser);

            //查詢當前使用者擁有的選單(模組)
            List<Module> menuList = moduleService.findModuleByUser(loginUser);
            //去除重複元素
            removeDuplicate(menuList);
            //選單必須存入session域
            session.setAttribute("menus",menuList);


            // 路徑: /WEB-INF/pages/home/main.jsp
            return "home/main";
        }else{
            //6.password不正確,提示"密碼錯誤"
            request.setAttribute("error","密碼錯誤");
            //手動轉發到login.jsp頁面
            return "forward:/login.jsp";
        }*/

//---------------------------------------------------以上是正常做法---------------------------------------------------
        //使用Shiro的登入認證邏輯

        //1.獲取Subject物件
        Subject subject = SecurityUtils.getSubject();

        //2.UsernamePasswordToken 封裝需要認證的資訊(使用者名稱和密碼)
        UsernamePasswordToken token = new UsernamePasswordToken(email,password);

        //3.呼叫Subject的login方法進行認證
        //4.判斷login方法有無異常,有異常代表認證失敗,無異常代表認證成功
        try {
            subject.login(token);

            //無異常代表認證成功

            //5.從Shiro的Subject中獲取登入使用者物件:  subject.getPrincipal()
            User loginUser = (User)subject.getPrincipal();

            //把登入使用者物件存入session域 (注意:這裡存入的loginUser物件,不是給Shiro使用的,是給我們自己業務使用的)
            session.setAttribute("loginUser",loginUser);

            //查詢當前使用者擁有的選單(模組)
            List<Module> menuList = moduleService.findModuleByUser(loginUser);
            //去除重複元素
            //若是已經在sql語句那裡加了distinct去重,就不需要這個了
            removeDuplicate(menuList);
            //選單必須存入session域
            session.setAttribute("menus",menuList);

            //跳轉到主頁
            return "home/main";
        } catch (UnknownAccountException e) {  //UnknownAccountException: 該異常代表使用者名稱不存在
            request.setAttribute("error","Shrio:使用者名稱不存在");
            return "forward:/login.jsp";
        } catch (IncorrectCredentialsException e) {  //IncorrectCredentialsException: 該異常代表密碼錯誤
            request.setAttribute("error","Shrio:密碼輸入有誤");
            return "forward:/login.jsp";
        } catch (Exception e) {  //其他異常
            request.setAttribute("error","Shrio:伺服器錯誤");
            return "forward:/login.jsp";
        }

    }

  第三步:編寫AuthRealm,實現登陸認證

/**
 * 自定義Realm
 */
public class AuthRealm extends AuthorizingRealm{
    @Autowired
    private UserService userService;

    //授權
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("執行授權方法...");
        return null;
    }

    //認證 (subject.login(token)方法固定執行doGetAuthenticationInfo方法)
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //System.out.println("執行認證方法...");

        //1.判斷使用者名稱是否存在
        //token傳給了AuthenticationToken
        UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
        String email = token.getUsername();

        User loginUser = userService.findByEmail(email);
        if(loginUser==null){
            //使用者名稱不存在
            //我們只需要return null即可,Shiro底層判斷為null則丟擲UnKnowAccountException異常
            return null;
        }

        //2.返回資料庫儲存密碼給Shiro,讓Shiro判斷密碼是否正確
        /**
         * Shiro底層獲取SimpleAuthenticationInfo的引數,例如獲取password判斷和使用者輸入的密碼是否一致
         *    1)如果密碼不一致,丟擲IncorrectCredentialsException異常
         *    2)如果密碼一致, 把principal存入session域
          */    

        //SimpleAuthenticationInfo: 封裝認證資料物件
        /**
         * 引數一: principal  登入使用者物件,用於Subject.getPrincipal()方法獲取
         * 引數二: password   資料庫的密碼
         * 引數三: realm的別名, 在多個Realm的情況下使用,用於區分使用者表的。只有一張使用者表不需要別名
         */
        return new SimpleAuthenticationInfo(loginUser,loginUser.getPassword(),"");
    }
}

總結:

Shiro登入認證的流程是怎樣的?

  LoginController的login方法呼叫Subject.login(),從而就到SecurityManager

  AuthRealm的認證方法,編寫判斷使用者名稱和返回資料庫密碼的邏輯,從資料庫獲得資料


8.加密

Shiro登陸認證(三)憑證匹配器-普通加密(如md5)

第一步:在applicationContext-shiro.xml,新增加密認證配置--shiro自帶的憑證匹配器

 <!--建立Shiro自帶的憑證匹配器-->
    <bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
        <!--指定需要加密的演算法名稱-->
        <property name="hashAlgorithmName" value="md5"/>
    </bean>

第二步:對資料庫密碼進行md5加密(select md5(123);),再直接修改資料庫的使用者密碼

請簡單說出Shiro加密判斷的執行流程

Subject.login(token) (未加密資料) -> AuthRealm的認證方法->返回資料庫密碼->先把token密碼使用演算法加密,再和資料庫的密碼匹配

Shiro登陸認證(四)憑證匹配器-加鹽加密

使用md5演算法加密1次 + 鹽(變數,每個使用者不同的)= 2次使用md5加密; 一般是使用郵箱作為鹽來第二次md5加密

步驟

1)編寫程式碼對密碼加鹽加密,檢視結果,把資料庫裡面的使用者密碼替換掉

public class Demo {

    public static void main(String[] args) {
        //1.原密碼
        String password = "123";

        //2.鹽
        String salt = "[email protected]";

        //3.加鹽加密
        /**
         * 引數一:原密碼
         * 引數二:鹽
         * 引數三(可選):加密次數,預設1次
         */
        Md5Hash md5Hash = new Md5Hash(password,salt);

        System.out.println(md5Hash.toString());
    }

}

  

2)編寫自定義憑證匹配器

public class CustomCredentialsMatcher extends SimpleCredentialsMatcher{

    /**
     * 完成密碼判斷的邏輯
     * @param token  包含了使用者輸入的密碼
     * @param info   包含了資料庫的密碼
     * @return 密碼判斷結果
     *       true: 代表資料庫的密碼和使用者輸入的密碼一致的
     *       false:代表資料庫的密碼和使用者輸入的密碼不一致的
     */
    
    //打個do就能出來這個方法
    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {

        //1.獲取使用者輸入的密碼
        UsernamePasswordToken userToken = (UsernamePasswordToken)token;
        
        //UsernamePasswordToken 是用來封裝使用者名稱和密碼,使用者名稱用字串封裝,密碼用字元陣列封裝
        //userToken.getPassword()['1','2','3']
        String userPassword = new String(userToken.getPassword()); // ['1','2','3']

        //獲取使用者郵箱
        String email = userToken.getUsername();

        //2.對使用者輸入的密碼進行加鹽加密
        Md5Hash md5Hash = new Md5Hash(userPassword,email);
        String encodePwd = md5Hash.toString();

        //3.獲取資料庫的密碼
        String dbPwd = (String)info.getCredentials();

        //4.判斷資料庫的密 碼和第二步產生的密碼是否一致,一致返回true,不一致返回false
        return dbPwd.equals(encodePwd);
    }
}

  

3)在applicationContext-shiro.xml,新增自定義憑證匹配器

 <!--3.配置Realm-->
    <bean id="myRealm" class="cn.itcast.web.shiro.AuthRealm">
        <!--配置憑證匹配器-->
        <property name="credentialsMatcher" ref="credentialsMatcher"/>
    </bean>
    
    <!--建立自定義憑證匹配器-->
    <bean id="credentialsMatcher" class="cn.itcast.web.shiro.CustomCredentialsMatcher"/>

  

4)測試

拓展注意:

1.新增使用者的時候要讓使用者的密碼加密,故再service實現類裡面寫新增使用者的方法是的業務邏輯是這樣的

 @Override
    public void save(User user) {
        //生成主鍵
        user.setId(UUID.randomUUID().toString());
        
        //對密碼加鹽加密
        Md5Hash md5Hash = new Md5Hash(user.getPassword(),user.getEmail());
        user.setPassword(md5Hash.toString());
        
        userDao.save(user);
    }

2.Shiro的登入登出程式碼實現

  @RequestMapping("/logout")
    public String logout(){
        //刪除session的登入數
        session.removeAttribute("loginUser");
        session.removeAttribute("menus");

        //Shiro登入登出(底層:刪除之前Shiro存入的session的key)
        //防止瀏覽器記住登入認證
        Subject subject = SecurityUtils.getSubject();
        subject.logout();

        return "redirect:/login.jsp";
    }

  

第二部分:Shiro授權(一)使用認證過濾器

如何實現授權?分為以下兩個步驟

  1. 登陸認證成功後,獲取使用者的許可權 (獲取許可權),就相當於買票

  2. 訪問資源時候,進行授權校驗:用訪問資源需要的許可權去使用者許可權(模組)列表查詢,如果存在,則有許可權訪問資源。(許可權攔截)。就相當於售票

.獲取許可權

實現自定義realm的doGetAuthorizationInfo()方法,返回使用者已經具有的許可權。

 //授權
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //System.out.println("執行授權方法...");

        //1、獲取當前登入使用者擁有的許可權
        //從Subject獲取當前登入使用者
        Subject subject = SecurityUtils.getSubject();
        User loginUser = (User)subject.getPrincipal();

        List<Module> moduleList = moduleService.findModuleByUser(loginUser);

        //2、把當前登入使用者的許可權告訴Shiro框架
        //2.1 建立授權物件
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //2.2 把模組存入授權物件
        if(moduleList!=null && moduleList.size()>0){
            for(Module module:moduleList){
                if(!StringUtils.isEmpty(module.getName())) {
                    //往授權物件存入授權資訊(授權資訊存入什麼字串? 字串必須唯一的)
                    info.addStringPermission(module.getName());
                }
            }

        }
        return info;
    }

  

.授權校驗

shiro其實提供了四種方式實現了許可權校驗:硬編碼,過濾器,註解方法,jsp標籤。主要掌握註解方法,jsp標籤

1)註解方法

  1. 在applicationContext-shiro.xml中開啟shiro註解支援

  <!--====開啟Shiro的註解====-->
    <!--
        Shiro註解藉助了Spring的AOP來實現授權校驗
    -->
    <bean id="lifecycleBeanPostProcessor"
          class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
    <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
          depends-on="lifecycleBeanPostProcessor"/>
    <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
        <property name="securityManager" ref="securityManager"/>
    </bean>

  2.開啟Aop自動代理(已經完成)

<!--6. 開啟Aop自動代理-->
<aop:aspectj-autoproxy/>

  3.在controller中使用@RequiresPermissions(“”)註解

2)再頁面上使用jsp標籤

<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>測試Shiro的jsp標籤</title>
</head>
<body>

<shiro:hasPermission name="企業管理">
<a href="">企業管理</a>
</shiro:hasPermission>

<hr/>

<shiro:hasPermission name="使用者管理">
<a href="">使用者管理</a>
</shiro:hasPermission>

</body>
</html>

  



最後,總結一下

1.shiro的關鍵程式碼:

  認證: subject.login(token);

  授權: subject.checkPermission("");

2.認證和授權的流程

Shiro vs Spri第二ng Security

Shiro vs Spring Security

Shiro vs Spring Security