Shiro的學習

Apache Shiro 是 Java 的一個安全(權限)框架。它能夠很是容易的開發出足夠安全的應用,其不只能夠用在 JavaSE 環境,也能夠用在 JavaEE 環境 。前端

Shiro 能夠完成:認證、受權、加密、會話管理、與Web 集成、緩存 等。下載:http://shiro.apache.org/  或  https://github.com/apache/shirojava

功能介紹

Shiro目標:Shiro開發團隊所稱的「應用程序安全」的四個基石——身份驗證、受權、會話管理和密碼git

  • Authentication:身份認證/登陸,驗證用戶是否是擁有相應的身份;
  • Authorization:受權,即權限驗證,驗證某個已認證的用戶是否擁有某個權限;即判斷用戶是否能進行什麼操做,如:驗證某個用戶是否擁有某個角色。或者細粒度的驗證某個用戶 對某個資源是否具備某個權限;
  • Session Manager:會話管理,即用戶登陸後就是一次會話,在沒有退出以前,它的全部信息都在會話中;會話能夠是普通 JavaSE 環境,也能夠是 Web 環境的;
  • Cryptography:加密,保護數據的安全性,如密碼加密存儲到數據庫,而不是明文存儲;

還有額外的功能來支持和加強:github

  • Web Support:Web 支持,能夠很是容易的集成到Web 環境;
  • Caching:緩存,好比用戶登陸後,其用戶信息、擁有的角色/權限沒必要每次去查,這樣能夠提升效率;
  • Concurrency:Shiro 支持多線程應用的併發驗證,即如在一個線程中開啓另外一個線程,就能把權限自動傳播過去;
  • Testing:提供測試支持,測試支持的存在是爲了幫助您編寫單元測試和集成測試,確保您的代碼將是安全的。
  • Run As:容許一個用戶僞裝爲另外一個用戶(若是他們容許)的身份進行訪問;
  • Remember Me:記住我,這個是很是常見的功能,即一次登陸後,下次再來的話不用登陸了

Shiro術語

  • Subject:應用代碼直接交互的對象是 Subject,也就是說 Shiro 的對外 API 核心就是 Subject。Subject 表明了當前「用戶」
  • Realm:Shiro 從 Realm 獲取安全數據(如用戶、角色、權限),就是說 SecurityManager 要驗證用戶身份,那麼它須要從 Realm 獲取相應的用戶進行比較以肯定用戶身份是否合法;也須要從 Realm 獲得用戶相應的角色/ 權限進行驗證用戶是否能進行操做。

Shiro架構

從外部來看Shiro ,即從應用程序角度的來觀察如何使用 Shiro 完成工做:web

  • Subject(org.apache.shiro.subject.Subject):應用代碼直接交互的對象是 Subject,也就是說 Shiro 的對外 API 核心就是 Subject。Subject 表明了當前「用戶」, 這個用戶不必定是一個具體的人,與當前應用交互的任何東西都是 Subject,如網絡爬蟲, 機器人等;與 Subject 的全部交互都會委託給 SecurityManager; Subject 實際上是一個門面,SecurityManager 纔是實際的執行者;
  • SecurityManager (org.apache.shiro.mgt.SecurityManager):安全管理器;即全部與安全有關的操做都會與 SecurityManager 交互;其管理着全部 Subject;能夠看出它是 Shiro 的核心,它負責與 Shiro 的其餘組件進行交互,它至關於 SpringMVC 中 DispatcherServlet 的角色
  • Realm (org.apache.shiro.realm.Realm):Shiro 從 Realm 獲取安全數據(如用戶、角色、權限),就是說 SecurityManager 要驗證用戶身份,那麼它須要從 Realm 獲取相應的用戶進行比較以肯定用戶身份是否合法;也須要從 Realm 獲得用戶相應的角色/ 權限進行驗證用戶是否能進行操做;能夠有一個或多個Realm來自定義,咱們在自定義的Realm中得到數據庫中真實的用戶名和密碼,來與傳入的用戶名和密碼進行比較。

從Shiro內部來看:spring

  • Authenticator(org.apache.shiro.authc.Authenticator):身份驗證負責 Subject 認證,執行和對驗證用戶(登陸),能夠自定義實現;可使用認證策略(Authentication Strategy),即什麼狀況下算用戶認證經過了;
  • Authorizer(org.apache.shiro.authz.Authorizer):受權器、即訪問控制器,用來決定主體是否有權限進行相應的操做;即控制着用戶能訪問應用中的哪些功能;
  • SessionManager (org.apache.shiro.session.mgt.SessionManager):管理 Session 生命週期的組件;而 Shiro 並不只僅能夠用在 Web 環境,也能夠用在如普通的 JavaSE 環境
  • CacheManager(org.apache.shiro.cache.CacheManager):緩存控制器,來管理如用戶、角色、權限等的緩存的;由於這些數據 基本上不多改變,放到緩存中後能夠提升訪問的性能
  • Cryptography (org.apache.shiro.crypto.*):密碼模塊,Shiro 提升了一些常見的加密組件用於如密碼加密/解密。

開始使用

咱們先導入Shiro的所需jar,咱們先經過構建一個JavaSE應用來把Shiro運行起來 ,能夠參照Shiro源碼包下 (shiro-shiro-root-1.3.2\samples\quickstart)的快速開始數據庫

<dependencies>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-all</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.16</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.6.4</version>
        </dependency>
</dependencies>

Shiro須要slf4j來作日誌,因此slf4j不能缺乏apache

添加配置文件

這些配置文件直接放在Src下,若是是Maven項目就放在resources瀏覽器

  • log4j.properties 文件來打印日誌,這裏就很少說了
  • shiro.ini 文件是Shiro配置角色和權限的文件,在與Spring整合以後就不用這個文件了,然而這裏仍是須要的

添加Java代碼來運行

咱們直接使用下載的Demo中quickstart下面的Quickstart.java類便可,它有一個main方法,咱們直接運行看到沒有異常並打印出日誌信息就是運行成功了緩存

Spring在Web環境整合Shiro

咱們通常狀況下都是使用Spring來整合Shiro,在Web環境下對URL進行驗證等工做。其經過一個 ShiroFilter 入口來攔截須要安全控制的URL,而後進行相應的控制

ShiroFilter 相似於如 Strut2/SpringMVC 這種 web 框架的前端控制器,是安全控制的入口點,其負責讀取配置(如ini 配置文件或Spring整合以後的filterChainDefinitions屬性),而後判斷URL是否須要登陸/權限等工做。

  1. 加入 Spring 和 Shiro 的 jar 包(這裏還須要ehCache的jar和配置文件,由於Shiro可使用ehCache來作cacheManager)
  2. 配置 Spring 及 SpringMVC

上面兩步就不細說了。下面開始配置文件(能夠參照:shiro-root-1.3.2-sourcerelease\shiro-root-1.3.2\samples\spring 配置 web.xml 文件和 Spring 的配置文件)

web.xml:主要配置關於Shiro的Filter

    <!-- Filters 配置Shiro過濾器-->
    <filter>
        <filter-name>shiroFilter</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
        <init-param>
            <param-name>targetFilterLifecycle</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>shiroFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

注意:這個filter-name的值,咱們須要它與Spring整合文件的 org.apache.shiro.spring.web.ShiroFilterFactoryBean 這個bean的id值一致

Spring整合文件,主要的配置有

  1. 配置 SecurityManager安全管理器
  2. 配置 CacheManager. (用戶受權信息Cache,可使用ehCache來作緩存)
  3. 配置 Realm (自定義Realm來實現認證)
  4. 配置 LifecycleBeanPostProcessor(Shiro bean在IOC容器的生命週期)
  5. 啓用 shiro 的註解
  6. 配置 ShiroFilter(它裏面ShiroFilterFactoryBean的id值須要與web.xml中的 DelegatingFilterProxy 這個Filter的名一致;咱們在 filterChainDefinitions 的屬性能夠配置URL過濾)
    <!--  
    1. 配置 SecurityManager安全管理器
    -->      
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="cacheManager" ref="cacheManager"/>
        <property name="sessionMode" value="native"/>
        <property name="realm" ref="jdbcRealm"/>
    </bean>

    <!--  
    2. 配置 CacheManager. (用戶受權信息Cache)
    2.1 須要加入 ehcache 的 jar 包及配置文件. 
    -->  
    <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
        <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/>
    </bean>

    <!-- 
        3. 配置 Realm 
        3.1 直接配置實現了 org.apache.shiro.realm.Realm 接口的 bean
    -->  
    <bean id="jdbcRealm" class="cn.lynu.realms.ShiroRealm"></bean>

    <!--  
    4. 配置 LifecycleBeanPostProcessor. 能夠自定的來調用配置在 Spring IOC 容器中 shiro bean 的生命週期方法. 
    -->  
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
    
    <!--  
    5. 啓用 IOC 容器中使用 shiro 的註解. 但必須在配置了 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>
    
 <!--  
    6. 配置 ShiroFilter. 
    6.1 id 必須和 web.xml 文件中配置的 DelegatingFilterProxy 的 <filter-name> 一致.
                      若不一致, 則會拋出: NoSuchBeanDefinitionException. 由於 Shiro 會來 IOC 容器中查找和 <filter-name> 名字對應的 filter bean.
                      能夠設置targetBeanName指定Shiro的filter名
    --> 
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <property name="loginUrl" value="/login.jsp"/>
        <property name="successUrl" value="/index.jsp"/>
        <property name="unauthorizedUrl" value="/unauthorized.jsp"/>
        
       <!--  
            配置哪些頁面須要受保護. 
            以及訪問這些頁面須要的權限. 
            1). anon 能夠被匿名訪問
            2). authc 必須認證(即登陸)後纔可能訪問的頁面. 
            3). logout 登出.
            4). roles 角色過濾器
        -->
        <property name="filterChainDefinitions">
            <value>
                /login.jsp = anon
                # everything else requires authentication:
                /** = authc
            </value>
        </property>
    </bean>

我這裏只配了一個login.jsp能夠在未登陸狀況下訪問,訪問其餘的頁面都會由於未受權而重定向到login.jsp

URL過濾器

其格式是:「url=過濾器[參數]」。若是當前請求的 url 匹配 [urls] 部分的某個 url 模式,將會執行其配置的過濾器。部分Shiro內置的過濾器:

過濾器 過濾器類 描述 例子
anon org,apache.shiro.web.filter.authc.AnonymousFilter 能夠直接訪問 /admin=anon
authc org,apache.shiro.web.filter.authc.FormAuthenticationFilter 必須認證(登陸)以後才能訪問 /admin=authc
logout org,apache.shiro.web.filter.authc.logoutFilter 註銷登陸:全部的session都會失效,全部身份都會失去關聯,remember Me Cookie也會刪除 /logout=logout
user org,apache.shiro.web.filter.authc.UserFilter 表示只要有用戶存在,不管是認證後或rememberMe均可以訪問 /admin=user
roles org,apache.shiro.web.filter.authc.RolesAuthorizcationFilter 角色過濾器,判斷當前用戶是否爲該角色。參數能夠有多個,多個參數必須加上引號,多個參數之間用逗號分隔 admin/**=roles["admin,guest"]

URL 匹配模式

url 模式使用 Ant 風格模式 ,Ant 路徑通配符支持 ?、 * 、 **,注意通配符匹配不包括目錄分隔符「/」:

  • ?:  匹配一個字符,如 /admin? 將匹配 /admin1,但不匹配 /admin 或 /admin/;
  • *:  匹配零個或多個字符串,如 /admin* 將匹配 /admin、 /admin123,但不匹配 /admin/1;
  • **: 匹配路徑中的零個或多個路徑,如 /admin/** 將匹 配 /admin/a 或 /admin/a/b

編碼式配置URL過濾器

其實咱們在Spring整合Shiro的配置文件的URL過濾器能夠經過編碼的方式配置,這樣咱們能夠將URL和過濾器配置到數據表中,經過編碼查詢數據庫的方式得到對應的關係,添加進一個 LinkedHashMap。咱們就建一個名爲 FilterChainDefinitionMapBuilder 的類,在這個類中寫一個 buildFilterChainDefinitionMap方法,咱們在這個方法中能夠查詢數據庫獲得URL和過濾器的對應關係,這裏就用模擬數據:

public class FilterChainDefinitionMapBuilder {

    public LinkedHashMap<String, String> buildFilterChainDefinitionMap(){

        LinkedHashMap<String, String> map = new LinkedHashMap<>();
        
        map.put("/login.jsp", "anon");
        map.put("/shiro/login", "anon");
        map.put("/shiro/logout", "logout");
        map.put("/user.jsp", "authc,roles[user]");
        map.put("/admin.jsp", "authc,roles[admin]");
        map.put("/list.jsp", "user");
        
        map.put("/**", "authc");
        
        return map;
    }
    

而後修改Spring整合Shiro配置文件的 filterChainDefinitions :

<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <property name="loginUrl" value="/login.jsp"/>
        <property name="successUrl" value="/list.jsp"/>
        <property name="unauthorizedUrl" value="/unauthorized.jsp"/>

        <property name="filterChainDefinitionMap" ref="filterChainDefinitionMap"></property>

</bean>
    
    <!-- 配置一個 bean, 該 bean 其實是一個 Map. 經過實例工廠方法的方式 -->
    <bean id="filterChainDefinitionMap" 
        factory-bean="filterChainDefinitionMapBuilder" factory-method="buildFilterChainDefinitionMap"></bean>
    
    <bean id="filterChainDefinitionMapBuilder"
        class="cn.lynu.factory.FilterChainDefinitionMapBuilder"></bean>

這個 cn.lynu.factory.FilterChainDefinitionMapBuilder 就是咱們寫的名爲 FilterChainDefinitionMapBuilder 的類,buildFilterChainDefinitionMap就是這個類的中寫的方法

加密

使用Shiro能夠對密碼進行加密操做,常見的MD5,SHA1都是支持的。咱們在前端得到密碼以後,Shiro會根據咱們的配置進行對應的加密,並於數據庫中的加密後字符串進行比較,比較成功以後就會放行進入受保護的頁面,比對失敗則會重定向到登陸頁。

咱們先寫個Controller,若是訪問該請求,handler類比Quickstart.java類訪問咱們自定義的realm:

package cn.lynu.controller;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
@RequestMapping("/shiro")
public class ShiroController {
    
    @RequestMapping("/login")
    public String login(@RequestParam("userName") String userName,
            @RequestParam("password") String password) {
        Subject currentUser = SecurityUtils.getSubject();
        if (!currentUser.isAuthenticated()) {
            //把用戶名和密碼封裝爲UsernamePasswordToken
            UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
            //Remember Me 操做
            token.setRememberMe(true);
            try {
                System.out.println("token:"+token.hashCode());
                //執行登陸
                currentUser.login(token);
            }
            //AuthenticationException是全部認證失敗的父類
            catch (AuthenticationException ae) {
              System.err.println("登陸失敗:"+ae);
            }
        }

        return "redirect:/index.jsp";
    } 

}

Subject的login方法就能夠訪問咱們配置的realm,咱們還須要在Spring的Shiro整合文件中配置這個URL爲anon(未登陸也可訪問)

再來看看咱們配置的realm,咱們先使用明文進行測試,這裏的自定義realm繼承於AuthenticatingRealm,AuthenticatingRealm實現了realm接口:

public class ShiroRealm extends AuthenticatingRealm{

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("doGetAuthenticationInfo1:"+token.hashCode());
        //1.將AuthenticationToken強轉爲UsernamePasswordToken
        UsernamePasswordToken uspaToken=(UsernamePasswordToken)token;
        //2.得到用戶名
        String userName=uspaToken.getUsername();
        //3.查詢數據庫得到真實的用戶名或密碼(這裏模擬)
        //3.1若用戶不存在,拋出UnknownAccountException
        if("unknown".equals(userName)) {
            throw new UnknownAccountException("用戶不存在");
        }
        //3.2根據用戶信息拋出其餘信息(這裏使用被鎖定,拋出LockedAccountException)
        if("lock".equals(userName)){
            throw new LockedAccountException("用戶被鎖定");
        }
        
        //4.根據用戶信息構建AuthenticationInfo,咱們經常使用其子類:
        //1).principal 用戶實體信息  能夠是userName,也能夠是數據表對應的實體類信息
        Object principal=userName;
        //2).credentials 密碼
        Object credentials="123";
        //3).realmName  使用當前realName便可
        String realmName=this.getName();
        SimpleAuthenticationInfo info=new SimpleAuthenticationInfo(principal, credentials, realmName);
        return info;
    }

這裏用戶名和密碼的在真實環境中應該從數據庫中查到,爲了不麻煩這裏只要用戶名不爲unknown或lock,密碼爲123就能夠登陸成功了。

若是用戶名爲unknown就拋一個 UnknownAccountException (不存在帳戶的異常);若是用戶名爲lock就拋一個LockedAccountException(帳戶鎖定異常),這倆個異常類都是AuthenticationException的子類,因此能夠拋出,AuthenticationException還有其餘的子類,咱們能夠根據用戶信息來拋異常的方式終止登陸。

登陸成功以後咱們就會根據在Controller中配置的重定向到index.jsp,在以前未登陸的狀況下,咱們是不能訪問這個頁面的

可是這裏有個問題:咱們知道了Shiro內部使用緩存(ehCache)來保存驗證,認證信息之類的,因此就出現了登陸成功以後再次回到登陸頁,再次登陸不管輸入什麼用戶名和密碼都會登陸成功,由於登陸成功的信息已經被緩存,這的時候就須要咱們進行登出操做:

<a href="shiro/logout">登出</a>

這裏簡單的使用一個超連接訪問URL->shiro/logout,咱們只須要在Spring的Shiro整合文件中配置這個URL爲logout便可

只須要在這裏配置一下,咱們點擊登出超連接以後就會重定向到login.jsp要求再次登陸

下面來到重頭戲:加密。這裏以MD5加密爲例,首先咱們須要在Spring的Shiro整合文件中配置自定義realm指定所需加密方式和加密次數:

加密次數與複雜度成正比,這樣配置以後前端傳過來的密碼就會被Shiro以MD5進行1024次加密

由於存在於加密後的字符串進行比較,因此咱們必須先知道加密後的結果,真實的使用固然是從數據庫中查出來,咱們這裏簡單點,使用Shiro提供的SimpleHash類來得到加密後的字符串。

在加密以前,咱們再來複習密碼學中的加鹽概念:相同的密碼通過加鹽加密以後就會獲得不一樣的加密字符串,這樣咱們將不一樣的加密字符串保存在數據庫中,也不會看得出這是相同密碼加密以後的結果,提升安全性。對於鹽值的要求:惟一不重複,對於每一個用戶使用其惟一的屬性,例如用戶名之類的做爲鹽值,這樣不一樣用戶即便密碼相同,加鹽加密以後也會獲得不一樣的加密字符串。

    public static void main(String[] args) {
        //加密方式
        String algorithmName="MD5";
        //密碼
        Object credentials="123";
        //鹽值(通常將一個惟一值做爲鹽值)
        Object salt=ByteSource.Util.bytes("admin");
        //加密次數
        int hashIterations=1024;
        SimpleHash simpleHash = new SimpleHash(algorithmName, credentials, salt, hashIterations);
        System.out.println(simpleHash);
    }

simpleHash就能夠獲得加密以後的字符串,這裏鹽值使用了Shiro提供的 ByteSource.Util.bytes 方法進行得到鹽值,這裏使用admin來得到鹽值,由於一會咱們直接使用用戶名爲admin(將至關於在使用用戶名這個惟一值來生成鹽值),密碼爲123進行登陸

修改咱們自定義Realm(Shirorealm)的doGetAuthenticationInfo方法:

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("doGetAuthenticationInfo1:"+token.hashCode());
        //1.將AuthenticationToken強轉爲UsernamePasswordToken
        UsernamePasswordToken uspaToken=(UsernamePasswordToken)token;
        //2.得到用戶名
        String userName=uspaToken.getUsername();
        //3.查詢數據庫得到真實的用戶名或密碼(這裏模擬)
        //3.1若用戶不存在,拋出UnknownAccountException
        if("unknown".equals(userName)) {
            throw new UnknownAccountException("用戶不存在");
        }
        //3.2根據用戶信息拋出其餘信息(這裏使用被鎖定,拋出LockedAccountException)
        if("lock".equals(userName)){
            throw new LockedAccountException("用戶被鎖定");
        }
        
        //4.根據用戶信息構建AuthenticationInfo,咱們經常使用其子類:
        //1).principal 用戶實體信息  能夠是userName,也能夠是數據表對應的實體類信息
        Object principal=userName;
        //2).credentials 密碼
        Object credentials=null;  //這裏使用加鹽密碼
        if("admin".equals(userName)) {
            credentials="c41d7c66e1b8404545aa3a0ece2006ac";
        }
        //3).realmName  使用當前realName便可
        String realmName=this.getName();
        //4).鹽值(原始密碼一致,可是經過加鹽加密以後的字符串會不同,提升安全性)
        ByteSource credentialsSalt=ByteSource.Util.bytes(userName);
        SimpleAuthenticationInfo info=new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
        return info;
    }

Ok,如今只有用戶名爲admin,密碼爲123的才能夠登陸成功

多個Realm

若是存在多個Realm,咱們應該如何配置呢?

1.這個時候Spring的Shiro整合文件就須要先配置多個Realm,這裏咱們配兩個:

    <bean id="jdbcRealm" class="cn.lynu.realms.ShiroRealm">
    <!-- 設置加密方式和加密次數 -->
       <property name="credentialsMatcher">
         <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
           <property name="hashAlgorithmName" value="MD5"></property>
           <property name="hashIterations" value="1024"></property>
         </bean>
       </property>
    </bean>
    
    <!--第二個realm  -->
    <bean id="secondRealm" class="cn.lynu.realms.ShiroRealm2">
    <!-- 設置加密方式和加密次數 -->
       <property name="credentialsMatcher">
         <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
           <property name="hashAlgorithmName" value="SHA1"></property>
           <property name="hashIterations" value="1024"></property>
         </bean>
       </property>
    </bean>

一個id爲jdbcRealm,另外一個id爲secondRealm

2.修改SecurityManager,以前使用一個realm,因此就直接將realm配置在SecurityManager裏面了,這裏咱們使用realms屬性替代:

    <!--  
    1. 配置 SecurityManager安全管理器
    -->      
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="cacheManager" ref="cacheManager"/>
        <property name="sessionMode" value="native"/>
<!--         配置單個realm
        <property name="realm" ref="jdbcRealm"/> -->
        <!--配置多個realm  -->
        <property name="authenticator" ref="authenticator"></property>
        <property name="realms">
            <list>
              <ref bean="jdbcRealm"/>
              <ref bean="secondRealm"/>
           </list>
        </property>
</property>
    </bean>

注意看這個 realms 值使用的是list,因此到驗證的時候順序就根據在這裏配置的realm前後順序進行驗證

配置後以後,咱們再來看看這個ShiroRealm2如何寫的(其實就是根據ShiroRealm改過來的,改成使用SHA1加密方式)

package cn.lynu.realms;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthenticatingRealm;
import org.apache.shiro.util.ByteSource;

public class ShiroRealm2 extends AuthenticatingRealm{

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("doGetAuthenticationInfo2:"+token.hashCode());
        //1.將AuthenticationToken強轉爲UsernamePasswordToken
        UsernamePasswordToken uspaToken=(UsernamePasswordToken)token;
        //2.得到用戶名
        String userName=uspaToken.getUsername();
        //3.查詢數據庫得到真實的用戶名或密碼(這裏模擬)
        //3.1若用戶不存在,拋出UnknownAccountException
        if("unknown".equals(userName)) {
            throw new UnknownAccountException("用戶不存在");
        }
        //3.2根據用戶信息拋出其餘信息(這裏使用被鎖定,拋出LockedAccountException)
        if("lock".equals(userName)){
            throw new LockedAccountException("用戶被鎖定");
        }
        
        //4.根據用戶信息構建AuthenticationInfo,咱們經常使用其子類:
        //1).principal 用戶實體信息  能夠是userName,也能夠是數據表對應的實體類信息
        Object principal=userName;
        //2).credentials 密碼
        Object credentials=null;  //這裏使用加鹽密碼
        if("admin".equals(userName)) {
            credentials="49d9fbf007fd95343492e607aa34455eeb062b26";
        }
        //3).realmName  使用當前realName便可
        String realmName=this.getName();
        //4).鹽值(原始密碼一致,可是經過加鹽加密以後的字符串會不同,提升安全性)
        ByteSource credentialsSalt=ByteSource.Util.bytes(userName);
        SimpleAuthenticationInfo info=new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
        return info;
    }
    
    public static void main(String[] args) {
        //加密方式
        String algorithmName="SHA1";
        //密碼
        Object credentials="123";
        //鹽值(通常將一個惟一值做爲鹽值)
        Object salt=ByteSource.Util.bytes("admin");
        //加密次數
        int hashIterations=1024;
        SimpleHash simpleHash = new SimpleHash(algorithmName, credentials, salt, hashIterations);
        System.out.println(simpleHash);
    }
    
}

爲了區分,在這兩個Realm的開始都打印一句: System.out.println("doGetAuthenticationInfo1:"+token.hashCode());   System.out.println("doGetAuthenticationInfo2:"+token.hashCode()); 用於區分不一樣的realm

運行以後會發現先打印的是doGetAuthenticationInfo1 後打印doGetAuthenticationInfo2,這與咱們在realms屬性中配置的順序有關

多個realm運行策略

多個Realm運行的前後順序咱們已經知道了與realms屬性有關,可是多個Realm用於驗證,以哪一個爲主呢?

    <!--配置多realm運行策略  -->
    <bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
        <property name="authenticationStrategy">
          <bean class="org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy"></bean>
        </property>
    </bean>

並加入進 securityManager 中管理

 <property name="authenticator" ref="authenticator"></property>

咱們其實在ModularRealmAuthenticator中配置了authenticationStrategy(驗證策略)爲AtLeastOneSuccessfulStrategy(只要有一個Realm驗證成功便可。這其實也是默認的驗證策略)。驗證策略還有:

  • FirstSuccessfulStrategy:只要有一個 Realm 驗證成功便可,只返回第 一個 Realm 身份驗證成功的認證信息,其餘的忽略
  • AtLeastOneSuccessfulStrategy:只要有一個Realm驗證成功便可,和 FirstSuccessfulStrategy 不一樣,將返回全部Realm身份驗證成功的認證信息
  • AllSuccessfulStrategy:全部Realm驗證成功纔算成功,且返回全部 Realm身份驗證成功的認證信息,若是有一個失敗就失敗了

這些驗證策略都是AuthenticationStrategy 接口的實現。咱們能夠修改驗證策略來知足咱們的需求

受權

有的時候咱們須要給用戶分配角色,用於區分不一樣角色的用戶均可以作什麼操做。這個時候使用的仍是自定義的Realm,以前咱們在驗證的時候是將自定義的realm繼承AuthenticatingRealm類(實現了realm接口),而咱們受權是將自定義的realm繼承AuthorizingRealm,AuthorizingRealm類是AuthenticatingRealm類的子類。驗證使用的是重寫AuthenticatingRealm的doGetAuthenticationInfo方法,而AuthorizingRealm類由於是子類也有這個方法,咱們也可使用這個方法完成驗證,而受權的方法是doGetAuthorizationInfo方法,咱們須要重寫它。這兩個類和方法都比較接近,注意區分。

public class ShiroRealm extends AuthorizingRealm{

    //用於驗證的方法
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("doGetAuthenticationInfo1:"+token.hashCode());
        //1.將AuthenticationToken強轉爲UsernamePasswordToken
        UsernamePasswordToken uspaToken=(UsernamePasswordToken)token;
        //2.得到用戶名
        String userName=uspaToken.getUsername();
        //3.查詢數據庫得到真實的用戶名或密碼(這裏模擬)
        //3.1若用戶不存在,拋出UnknownAccountException
        if("unknown".equals(userName)) {
            throw new UnknownAccountException("用戶不存在");
        }
        //3.2根據用戶信息拋出其餘信息(這裏使用被鎖定,拋出LockedAccountException)
        if("lock".equals(userName)){
            throw new LockedAccountException("用戶被鎖定");
        }
        
        //4.根據用戶信息構建AuthenticationInfo,咱們經常使用其子類:
        //1).principal 用戶實體信息  能夠是userName,也能夠是數據表對應的實體類信息
        Object principal=userName;
        //2).credentials 密碼
        Object credentials=null;  //這裏使用加鹽密碼
        if("admin".equals(userName)) {
            credentials="c41d7c66e1b8404545aa3a0ece2006ac";
        }
        if("user".equals(userName)) {
            credentials="2bbffae8c52dd2532dfe629cecfb2c85";
        }
        //3).realmName  使用當前realName便可
        String realmName=this.getName();
        //4).鹽值(原始密碼一致,可是經過加鹽加密以後的字符串會不同,提升安全性)
        ByteSource credentialsSalt=ByteSource.Util.bytes(userName);
        SimpleAuthenticationInfo info=new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
        return info;
    }

    //用於受權的方法
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //1. 從 PrincipalCollection 中來獲取登陸用戶的信息
        Object principal = principals.getPrimaryPrincipal();
        
        //2. 利用登陸的用戶的信息來用戶當前用戶的角色或權限(可能須要查詢數據庫)
        Set<String> roles = new HashSet<>();
        roles.add("user");
        if("admin".equals(principal)){
            roles.add("admin");
        }
        
        //3. 建立 SimpleAuthorizationInfo, 並設置其 roles 屬性.
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
        
        //4. 返回 SimpleAuthorizationInfo 對象. 
        return info;
    }
    
}

doGetAuthorizationInfo方法中默認給全部用戶賦予的是user的角色,而若是用戶名爲admin的用戶還會額外賦予admin角色。別忘了在Spring的Shiro的配置文件中配置這個自定義的realm

user和admin角色均可以作什麼呢?

咱們在Spring與Shiro的整合配置文件中

 /user.jsp=authc,roles[user]
 /admin.jsp=authc,roles[admin]

user.jsp只有在認證和擁有user角色的狀況下才能夠訪問,而admin.jsp在認證和擁有admin角色的狀況下才能夠訪問

如今的user.jsp和admin.jsp只有在登陸以後並擁有對應權限的用戶在能夠訪問。

Shiro標籤

Shiro提供了一套標籤供咱們在JSP頁面進行權限控制,首先在頁面導入Shiro標籤庫

<%-- 使用Shiro標籤 --%>
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>

guest標籤:用戶沒有身份驗證時顯示對應信息,及遊客能夠看到的信息

<shiro:guest>
       歡迎遊客訪問,<a href="login.jsp">登陸</a>
</shiro:guest>

user標籤:用戶已經認證/記住我登陸以後顯示的信息

<shiro:user>
         歡迎[<shiro:prinicpal/>]登陸,<a href="logout">退出</a>
</shiro:user>

authenticated標籤:用戶已經身份驗證經過,即經過Subject.logon登陸成功,而不是經過記住我登陸,顯示的信息

<shiro:authenticated>
        用戶[<shiro:principal/>]已經身份驗證經過
</shiro:authenticated>

notAuthenticated標籤:用戶未進行身份認證,即沒經過Subject.login進行登陸,記住我自動登陸也屬於爲未通過身份驗證。

<shiro:notAuthenticated>
            未身份驗證(包括記住我)
<shiro:notAuthenticated>

principal標籤:顯示用戶身份信息.即調用了Subject.getPrincipal()得到PrimaryPrincipal。

<shiro:principaol/>

hasRole標籤:若是當前Subject有角色將顯示內容

<shiro:hasRole name="admin">
           用戶[<shiro:principal/>]擁有admin角色<be>
</shiro:hasRole>

hasAnyRoles標籤:若是當前Subject有任意一個角色(或的關係)將顯示內容

<shiro:hasAnyRoles name="admin,user">
        用戶[<shiro:principal/>]擁有角色admin或user<br>
</shiro:hasAnyRoles>

lacksRole標籤:若是當前subject沒有對應角色將顯示的內容

<shiro:lacksRole name="admin">
              用戶[<shiro:principal/>]沒有角黑色admin<br>
</shiro:lacksRole>

hasPermission標籤: 若是當前Subject有權限則顯示內容

<shiro:hasPermission name="user:create">
        用戶[<shiro:principal/>]擁有權限user:create<br>
</shiro:hasPermission>

lacksPermission標籤:若是當前Subject沒有權限將顯示內容

<shiro:lacksPermission name="user:create">
       用戶[<shiro:principal/>]沒有權限user:create
</shiro:lacksPermission>

權限註解

@RequiresAuthentication:表示Subject已經經過login進行了身份認證;即Subject.isAuthenticated()返回true

@RequiresUser:表示當前Subject已經經過身份驗證或記住我登陸

@RequiresGuest:表示是當前Subject沒有身份認證或記住我登陸過,便是遊客身份

@RequiresRoles(values={"admin","user"},logical=Logical.OR):表示當前Subject須要角色admin或user

我在Service層配置一個方法只有擁有admin在可運行:

    @RequiresRoles(value={"admin"})
    public void testMethod(){
        System.out.println("Now Date:"+new Date());
    }

這樣只有擁有admin角色的用戶調用這個方法纔會打印日期,其餘角色的用戶調用這個方法不會成功,會拋一個異常:

咱們能夠根據這個異常作一個異常處理:只要出現這個異常就轉到一個頁面,提示一些東西

可是我在測試的時候遇到一些問題:若是使用註解標註Service那麼Shiro就失效了,只有用XML中聲明式的配置Service才能夠?

會話管理

Shiro提供了完整的會話管理,它不依賴於底層容器(如web容器tomcat),無論JavaSE仍是JavaEE均可以使用,提供了會話管理,會話監聽,失效/過時支持等等

會話相關的API

  • Subject.getSession():便可得到會話;Subject.getSession(true)若是當前沒有會話就建立一個;Subject.getSession(false)若是當前沒有Session就返回null
  • session.getId():得到當前會話的惟一標識
  • session.getHost():獲取當前Subject的主機地址
  • session.getTimeout()&session.getTimeout(毫秒):得到/設置當前會話session的過時時間
  • session.stop():銷燬會話
  • session.setAttribute(key,val) & session.getAttribute(key) & session.removeAttribute(key):設置/獲取/刪除會話中的屬性

這裏咱們完成一個操做:在Controller層往Session中放入一個名爲userName的lz值,並在Service層取出這個userName值,不要將HttpSession傳入給Service層避免代碼侵入,使用Shiro的Session得到這個值

Controller:

    @RequestMapping("/testShiroAnnotation")
    public String testShiroAnnotation(HttpSession session){
        session.setAttribute("userName", "lz");
        shiroService.testMethod();
        return "redirect:/list.jsp";
    }

Service:

    public void testMethod(){

        Session session = SecurityUtils.getSubject().getSession();
        Object val = session.getAttribute("userName");
        
        System.out.println("Service SessionVal: " + val);
    }

RememberMe

Shiro 提供了記住我(RememberMe)的功能,好比訪問如淘寶 等一些網站時,關閉了瀏覽器,下次再打開時仍是能記住你是誰, 下次訪問時無需再登陸便可訪問,基本流程以下:

  1. 首先在登陸頁面選中 RememberMe 而後登陸成功;若是是 瀏覽器登陸,通常會把 RememberMe 的Cookie 寫到客戶端並保存下來;
  2. 關閉瀏覽器再從新打開;會發現瀏覽器仍是記住你的;
  3. 訪問通常的網頁服務器端仍是知道你是誰,且能正常訪問;
  4. 可是好比咱們訪問淘寶時,若是要查看個人訂單或進行支付 時,此時仍是須要再進行身份認證的,以確保當前用戶仍是你。

咱們以前其實一直使用了rememberMe功能:

            //把用戶名和密碼封裝爲UsernamePasswordToken
            UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
            //Remember Me 操做
            token.setRememberMe(true);

咱們能夠在前端加一個checkbox用於判斷是否須要rememberMe,若是勾選,這個設置setRememberMe  爲true

由於rememberMe底層使用的是Cookies,咱們能夠在Spring整合Shiro的配置文件的securityManager配置rememberMeManager.cookie.maxAge

        <!--設置remember Me 的Cookie時間 單位秒  -->
        <property name="rememberMeManager.cookie.maxAge" value="1800"></property>

認證和記住我

  • subject.isAuthenticated() 表示用戶進行了身份驗證登陸的, 即便用 Subject.login 進行了登陸;
  • subject.isRemembered():表示用戶是經過記住我登陸的, 此時可能並非真正的你(如你的朋友使用你的電腦,或者 你的cookie 被竊取)在訪問的
  • 二者二選一,即 subject.isAuthenticated()==true,則 subject.isRemembered()==false;反之同樣。

建議

  • 訪問通常網頁:如我的在主頁之類的,咱們使用user 攔截器便可,user 攔截器只要用戶登陸 (isRemembered() || isAuthenticated())過便可訪問成功;
  • 訪問特殊網頁:如個人訂單,提交訂單頁面,咱們使用 authc 攔截器便可,authc 攔截器會判斷用戶是不是經過 Subject.login(isAuthenticated()==true)登陸的,若是是才放行,不然會跳轉到登陸頁面從新登陸。
相關文章
相關標籤/搜索