apache shiro學習筆記

1、權限概述

1.1 認證與受權

  認證:系統提供的用於識別用戶身份的功能,一般登陸功能就是認證功能-----讓系統知道你是誰??css

  受權:系統授予用戶能夠訪問哪些功能的許可(證書)----讓系統知道你能作什麼??html

1.2 常見的權限控制方式

  【URL攔截權限控制】——底層基於攔截器或者過濾器實現前端

  【方法註解權限控制】——底層基於代理技術實現,爲Action建立代理對象,由代理對象進行權限校驗java

2、apache shiro框架簡介

2.1 shiro簡介

  Apache Shiro(發音爲「shee-roh」,日語「堡壘(Castle)」的意思)是一個強大易用的Java安全框架(官方網站:shiro.apache.org),提供了認證、受權、加密和會話管理功能,可爲任何應用提供安全保障 - 從命令行應用、移動應用到大型網絡及企業應用。mysql

  如下是你能夠用 Apache Shiro所作的事情:web

  • 驗證用戶
  • 對用戶執行訪問控制。如: 判斷用戶是否擁有角色admin;判斷用戶是否擁有訪問的權限
  • 在任何環境下使用 Session API。例如CS程序。
  • 可使用多個用戶數據源。例如一個是oracle用戶庫,另一個是mysql用戶庫。
  • 單點登陸(SSO)功能。 
  • 「Remember Me」服務 ,相似購物車的功能,shiro官方建議開啓。

2.2 體系結構

  

  Shiro的4大部分——身份驗證,受權,會話管理和加密:spring

  • Authentication:身份認證/登陸,驗證用戶的身份信息;
  • Authorization:受權,哪一個用戶擁有什麼樣的權限給其進行受權。
  • Session Management:會話管理,用戶登陸後未退出時其信息都存在會話裏。
  • Cryptography:加密,對用戶密碼進行加密,防止明文密碼出現。

  除了以上功能,shiro還提供不少擴展:sql

  • Web Support:Web支持,能夠很是容易的集成到Web環境;
  • Caching:緩存可使應用程序運行更有效率。 
  • Concurrency:多線程相關功能。
  • Testing:幫助咱們進行測試相關功能 
  •  "Run As":一個容許用戶假設爲另外一個用戶身份(若是容許)的功能,有時候在管理腳本頗有用。 
  • Remember Me:記住用戶身份,這個功能開啓後下次登陸就不能從新登陸了,相似購物車功能。

2.3 shiro的工做流程

  工做流程是這樣的:前臺將用戶名/密碼經過Subject與shiro的核心管理器(shiro securitymanager)進行交互來獲取權限和認證,核心管理器從Realm中獲取用戶的權限信息。數據庫

  • Subject:主體,表明了當前「用戶」,這個用戶能夠是人也能夠是某個機器。全部Subject 實例都必須綁定到一個SecurityManager上。
  • SecurityManager:安全管理器;這是shiro的核心,它管理着全部Subject,初始化時協調各個模塊運行。然而,一旦 SecurityManager協調完畢,SecurityManager 會被單獨留下,且咱們只須要去操做Subject便可,無需操做SecurityManager 。 可是咱們得知道,當咱們正與一個 Subject 進行交互時,實質上是 SecurityManager在處理 Subject 安全操做。
  • Realm:Shiro從Realm獲取安全數據(如用戶、角色、權限),能夠把Realm當作DataSource,即安全數據源。他獲取安全數據來判斷subject是否可以登陸,subject擁有什麼權限。他有點相似DAO。在配置realms時,須要至少一個realm。並且Shiro提供了一些經常使用的 Realms來鏈接數據源,如LJDBC數據源的JdbcRealm,properties文件數據源的PropertiesRealm,等等。咱們也能夠插入本身的 Realm實現來表明自定義的數據源。 像其餘組件同樣,Realms也是由SecurityManager控制

  

3、使用shiro實現登陸安全認證

  shiro的優點,不須要在代碼裏面判斷是否登陸,是否有執行的權限,實現了從前端頁面到後臺代碼的權限的控制很是的靈活方便。apache

  傳統的登陸認證方式是,從前端頁面獲取到用戶輸入的帳號和密碼以後,直接去數據庫查詢帳號和密碼是否匹配和存在,若是匹配和存在就登陸成功,沒有就提示錯誤。

  而shiro的認證方式則是,從前端頁面獲取到用戶輸入的帳號和密碼以後,傳入給一個UsernamePasswordToken對象也就是令牌,而後再把令牌傳給subject,subject會調用自定義的 realm,realm作的事情就是用前端用戶輸入的用戶名,去數據庫查詢出一條記錄(只用用戶名去查,查詢拿到返回用戶名和密碼),而後再把兩個密碼進行對比,不一致就拋出異常。也就是說若是subject.login(token);沒有拋出異常,就表示用戶名和密碼是匹配的,表示登陸成功

  【實現步驟】:

  第一步:在父工程的pom.xml中引入shiro框架相關的jar

    <!-- 引入shiro框架的依賴 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-all</artifactId>
            <version>1.2.2</version>
        </dependency>

  第二步:在web.xml中配置spring框架提供的用於整合shiro框架的過濾器

<!--配置過濾器,用於整合shiro-->
<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>

   第三步:在spring配置文件中配置bean,id爲shiroFilter

<!--配置shiro框架的過濾器工廠bean-->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <!--shiro的核心安全接口-->
    <property name="securityManager" ref="securityManager"/>
    <!--沒有登陸的用戶請求須要登陸的頁面時自動跳轉到登陸頁面,不是必須的屬性,不輸入地址的話會自動尋找項目web項目的根目錄下的」/login.jsp」頁面。-->
    <property name="loginUrl" value="/login.jsp"/>
    <!--登陸成功默認跳轉頁面,不配置則跳轉至"/"。-->
    <property name="successUrl" value="/index.jsp"/>
    <!--未受權時跳轉的頁面-->
    <property name="unauthorizedUrl" value="unauthorized.jsp"/>
    <!--指定URL級別攔截策略-->
    <property name="filterChainDefinitions">
        <value>
            /css/** = anon
            /js/** = anon
            /images/** = anon
            /login.jsp* = anon
            /validatecode.jsp* = anon
            /userAction_login = anon
        <!-- 攔截page_base_staff.action這個方法必須有staff-list權限才能使用 -->
            /page_base_staff.action = perms["staff-list"]
            /* = authc
        </value>
    </property>
</bean>

   shiro框架提供的過濾器:

  

  anon:例子/admins/**=anon 沒有參數,表示能夠匿名使用,無需校驗權限。

  authc:例如/admins/user/**=authc表示須要認證(登陸)才能使用,沒有參數。

  perms:例如/page_base_staff.action=perms["staff-list"]表示須要有"staff-list"這個權限才能查看;參數能夠寫多個,多個時必須加上引號,而且參數之間用逗號分割,例如/admins/user/**=perms["user:add:*,user:modify:*"],當有多個參數時必須每一個參數都經過才經過,想當於isPermitedAll()方法。

  roles:例子/admins/user/**=roles[admin],當前用戶是否有這個角色權限。 

  第四步:配置安全管理器

<!--配置安全管理器-->
<bean id = "securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"/>

  第五步:編寫登陸方法

  • 傳統的登陸方法:
    /**
         * 用戶登陸
         */
        public String login(){
            //從Session中獲取生成的驗證碼
            String validatecode = (String) ServletActionContext.getRequest().getSession().getAttribute("key");
            //校驗驗證碼是否輸入正確
            if(StringUtils.isNotBlank(checkcode) && checkcode.equals(validatecode)){
                //輸入的驗證碼正確
                User user = userService.login(model);
                if(user != null){
                    //登陸成功,將user對象放入session,跳轉到首頁
                    ServletActionContext.getRequest().getSession().setAttribute("loginUser", user);
                    return HOME;
                }else{
                    //登陸失敗,,設置提示信息,跳轉到登陸頁面
                    //輸入的驗證碼錯誤,設置提示信息,跳轉到登陸頁面
                    this.addActionError("用戶名或者密碼輸入錯誤!");
                    return LOGIN;
                }
            }else{
                //輸入的驗證碼錯誤,設置提示信息,跳轉到登陸頁面
                this.addActionError("輸入的驗證碼錯誤!");
                return LOGIN;
            }
        }
  • shiro的登陸認證方法

    /**
         * 用戶登陸,使用shiro框架提供的方式進行認證操做
         */
        public String login() {
            // 從session中獲取生成的驗證碼
            String validateCode = (String) ServletActionContext.getRequest().getSession().getAttribute("key");
            // 驗證驗證碼是否正確
            if (StringUtils.isNotBlank(checkcode) && checkcode.equals(validateCode)) {
                // 輸入的驗證碼正確
                // 使用shiro框架提供的方式進行認證
                Subject subject = SecurityUtils.getSubject(); //得到當前登陸用戶對象,如今狀態爲"未認證"
                // 用生成令牌(傳入用戶輸入的帳號和密碼)
                AuthenticationToken token = new UsernamePasswordToken(model.getUsername(), MD5Utils.md5(model.getPassword()));
                // 認證登陸
                try {
                    // 這裏會加載自定義的realm
                    subject.login(token); //把令牌放到login裏面進行查詢,若是查詢帳號和密碼時候匹配,若是匹配就把user對象獲取出來,失敗就拋異常
                    //獲取登陸成功的用戶對象(之前是直接去service裏面查)
                    User user = (User) subject.getPrincipal();
                    ServletActionContext.getRequest().getSession().setAttribute("loginUser", user);
                    return "home";
                } catch (Exception e) {
                    //認證登陸失敗拋出異常
                    this.addActionError("用戶名和密碼錯誤");
                    return LOGIN;
                }
            } else {
                // 輸入的驗證碼錯誤,設置提示信息,跳轉到登陸頁面
                this.addActionError("輸入的驗證碼錯誤");
                return LOGIN;
            }
        }

  第六步:自定義realm

public class BOSRealm extends AuthorizingRealm {
    @Autowired
    private IUserDao userDao;

    // 受權方法
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // TODO Auto-generated method stub
        return null;
    }

    //認證方法
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 獲取令牌
        UsernamePasswordToken mytoken = (UsernamePasswordToken) token;
        // 獲得帳號和密碼
        String username = mytoken.getUsername();
        // 根據用戶名查詢數據庫是否存在用戶,若是存在返回對象(帳號和密碼都有的對象)
        User user = userDao.findUserByUsername(username);
        if (user == null) {
            // 用戶名不存在
            return null;
        }
        // 若是能查詢到,再由框架對比數據庫中查詢到的密碼與頁面提交的密碼是否一致
        // 參數1:用戶認證的對象(subject.getPrincipal();返回的對象)
        // 參數2.從數據庫根據用戶名查詢到的用戶的密碼
        // 參數3.把當前自定義的realm對象傳給SimpleAuthenticationInfo,在配置文件須要注入
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), this.getName());
        return info;
    }
}

   第七步:在安全管理器裏面注入自定義的realm

<!--配置安全管理器-->
<bean id = "securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <!--注入realm到安全管理器進行密碼匹配-->
    <property name="realm" ref="bosRealm"/>
</bean>

<!--註冊自定義realm-->
<bean id = "bosRealm" class="cn.itcast.bos.realm.BOSRealm"/>

4、使用shiro爲用戶受權

4.1 添加權限的四種方式

  • URL攔截權限控制——基於過濾器實現
    <!-- 配置URL攔截規則 -->
    <property name="filterChainDefinitions">
       <value>
           /css/** = anon
           /js/** = anon
           /images/** = anon
           /validatecode.jsp* = anon
           /login.jsp* = anon
           /User_login.action= anon
          <!-- 攔截page_base_staff.action這個方法必須有staff權限才能使用 -->
           /page_base_staff.action = perms["staff"]  /** = authc 
    </value>
    </property>
  • 方法註解權限控制——基於代理技術實現

   第一步:在spring配置文件中開啓shiro註解支持

<!--開啓shiro框架註解支持-->
<bean id="defaultAdvisorAutoProxyCreator" class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator">
    <!--必須使用cglib方式爲Action對象建立代理對象-->
    <property name="proxyTargetClass" value="true"/>
</bean>

<!--配置shiro框架提供的切面類,用於建立代理對象-->
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"></bean>

   第二步:在Action的方法上使用shiro註解

/**
 * 取派員批量刪除
 * @return
 */ @RequiresPermissions("staff-delete") //執行這個方法,須要當前用戶具備staff-delete權限
public String deleteBatch() {
    staffService.deleteBatch(ids);
    return "list";
}

  第三步:在struts.xml中配置全局異常捕獲,當shiro框架拋出權限不足異常時,跳轉到權限不足提示頁面

<!--全局結果集定義-->
<global-results>
    <result name="login">/login.jsp</result>
    <result name="unauthorized">/unauthorized.jsp</result>
</global-results>

<global-exception-mappings>
    <exception-mapping exception="org.apache.shiro.authz.UnauthorizedException" result="unauthorized"></exception-mapping>
</global-exception-mappings>
  •  頁面標籤權限控制——標籤技術實現

  第一步:在jsp頁面中引入shiro的標籤庫   

<%@ taglib uri="http://shiro.apache.org/tags" prefix="shiro"%>

  第二步:使用shiro的標籤控制頁面元素展現  

<!--有staff-delete權限才能顯示此an按鈕-->
<shiro:hasPermission name="staff-delete">
{
    id : 'button-delete',
    text : '刪除',
    iconCls : 'icon-cancel',
    handler : doDelete
},
</shiro:hasPermission>
  • 代碼級別權限控制(幾乎不用)——基於代理技術實現

  在要設置權限的代碼中添加一下兩行代碼就能夠了

    //修改
    public String edit()
    {
        Subject subject = SecurityUtils.getSubject();
        subject.checkPermission("staff.edit");//要運行此方法下面的代碼,必需要擁有staff.edit的權限
        //更新model
        staffService.update(model);
        return "staff";
    }

4.2 受權    

  • 手動受權和認證

  由於要受權的權限太多,因此須要一張權限表

public class BOSRealm extends AuthorizingRealm {
    @Autowired
    private IUserDao userDao;

    // 受權方法
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

        // 爲用戶受權
        info.addStringPermission("staff");//爲page_base_staff.action請求受權staff權限
        info.addStringPermission("staff.delete");//爲page_base_staff.action請求受權staff權限
        info.addStringPermission("staff.edit");
        return info;
    }

    //認證方法
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 獲取令牌
        UsernamePasswordToken mytoken = (UsernamePasswordToken) token;
        // 獲得帳號和密碼
        String username = mytoken.getUsername();
        // 根據用戶名查詢數據庫是否存在用戶,若是存在返回對象(帳號和密碼都有的對象)
        User user = userDao.findUserByUsername(username);
        if (user == null) {
            // 用戶名不存在
            return null;
        }
        // 若是能查詢到,再由框架對比數據庫中查詢到的密碼與頁面提交的密碼是否一致
        // 參數1:用戶認證的對象(subject.getPrincipal();返回的對象)
        // 參數2.從數據庫根據用戶名查詢到的用戶的密碼
        // 參數3.把當前自定義的realm對象傳給SimpleAuthenticationInfo,在配置文件須要注入
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), this.getName());
        return info;
    }
}
  • 遍歷數據庫受權

  根據當前登陸用戶查詢數據庫,獲取實際對應的權限

// 受權方法
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

    // 獲取當前登陸用戶對象
    User user = (User) SecurityUtils.getSubject().getPrincipal();
    // 根據當前登陸用戶查詢數據庫,獲取實際對應的權限
    List<Function> list = null;
    if (user.getUsername().equals("admin")) {
        DetachedCriteria detachedCriteria = DetachedCriteria.forClass(Function.class);
        // 超級管理員內置用戶,查詢全部權限
        list = functionDao.findByCriteria(detachedCriteria);
    } else {
        list = functionDao.findFunctionByUserId(user.getId());
    }
    for (Function function : list) {
        info.addStringPermission(function.getCode());
    }

    return info;
}

5、使用ehcache緩存權限數據

  ehcache是專門緩存插件,能夠緩存Java對象,提升系統性能。

5.1 配置ehcache緩存

  第一步:在pom.xml文件中引入ehcache的依賴

<!-- 引入ehcache的依賴 -->
<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache-core</artifactId>
    <version>2.6.6</version>
</dependency>

   第二步:在項目中提供ehcache的配置文件(ehcache.xml)

<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">

    <diskStore path="java.io.tmpdir"/>
    <defaultCache
            maxElementsInMemory="10000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            overflowToDisk="true"
            maxElementsOnDisk="10000000"
            diskPersistent="false"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LRU"
            />
</ehcache>

  第三步:在spring配置文件中配置緩存管理器對象,並注入給安全管理器對象

<!--配置安全管理器-->
<bean id = "securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <!--注入realm到安全管理器進行密碼匹配-->
    <property name="realm" ref="bosRealm"/>
    <!--注入緩存管理器-->
    <property name="cacheManager" ref="cacheManager"/>
</bean>

<!--註冊緩存管理器-->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
    <!--注入ehcache的配置文件-->
    <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/>
</bean>

5.2 測試緩存的做用

  上面咱們已經配置好了緩存,那麼咱們怎麼證實緩存是否起做用了呢?咱們能夠經過給Realm打斷點的方式來測試:

  

  沒配置ehcache緩存前,每次點擊查詢頁面,都會執行這個方法。

  而配置了緩存後,只有當咱們第一次訪問這個查詢頁面的時候,纔會執行一次這個方法,即只用查詢一次數據庫,之後就不用查(權限數據)了。

  那麼何時會再次查數據庫呢?咱們能夠經過ehcache.xml配置它的有效時間,默認是(從不操做開始)2分鐘後再次查詢,咱們能夠修改相關配置來肯定它的有效時間,(好比設置6秒後再次查詢數據庫):

<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">

    <diskStore path="java.io.tmpdir"/>
    <defaultCache
            maxElementsInMemory="10000"
            eternal="false"
            timeToIdleSeconds="6"
            timeToLiveSeconds="6"
            overflowToDisk="true"
            maxElementsOnDisk="10000000"
            diskPersistent="false"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LRU"
            />
</ehcache>

  若是,咱們從新登陸用戶,有效時間也會從新生效。每次咱們從新登陸後,原先緩存的數據就沒了,哪怕你設置的時間還沒到。

 

 

 

 

 

 

 

 

 

 

 參考:https://blog.csdn.net/liaomin416100569/article/details/78838900

  https://baijiahao.baidu.com/s?id=1591438032280398708&wfr=spider&for=pc

  https://www.cnblogs.com/AngeLeyes/p/7196956.html

相關文章
相關標籤/搜索