1. 程式人生 > >Shiro(1.3.2)——整合Spring以及具體實現

Shiro(1.3.2)——整合Spring以及具體實現

1.Web.xml配置:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns="http://java.sun.com/xml/ns/javaee"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
	id="WebApp_ID" version="2.5">
	
	<!-- needed for ContextLoaderListener -->
	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>classpath:applicationContext.xml</param-value>
	</context-param>

	<!-- Bootstraps the root web application context before servlet initialization -->
	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>
	
	<!-- The front controller of this Spring Web application, responsible for handling all application requests -->
	<servlet>
		<servlet-name>spring</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<load-on-startup>1</load-on-startup>
	</servlet>

	<!-- Map all requests to the DispatcherServlet for handling -->
	<servlet-mapping>
		<servlet-name>spring</servlet-name>
		<url-pattern>/</url-pattern>
	</servlet-mapping>
	
	<!-- Shiro Filter is defined in the spring application context: -->
	<!-- 
	1. 配置  Shiro 的 shiroFilter.  
	2. DelegatingFilterProxy 實際上是 Filter 的一個代理物件. 預設情況下, Spring 會到 IOC 容器中查詢和 
	<filter-name> 對應的 filter bean. 也可以通過 targetBeanName 的初始化引數來配置 filter bean 的 id. 
	-->
    <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>
	
</web-app>

2.applicationContext.xml:

<?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">

    <!-- =========================================================
         Shiro Core Components - Not Spring Specific
         ========================================================= -->
    <!-- Shiro's main business-tier object for web-enabled applications
         (use DefaultSecurityManager instead when there is no web environment)-->
    <!--  
    1. 配置 SecurityManager!
    -->     
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="cacheManager" ref="cacheManager"/>
        <property name="authenticator" ref="authenticator"></property>
        
        <property name="realms">
        	<list>
    			<ref bean="jdbcRealm"/>
    			<ref bean="secondRealm"/>
    		</list>
        </property>
        
        <!--  
        設定 cookie的有效時長
        -->  
        <property name="rememberMeManager.cookie.maxAge" value="10"></property>
    </bean>

    <!-- Let's use some enterprise caching support for better performance.  You can replace this with any enterprise
         caching framework implementation that you like (Terracotta+Ehcache, Coherence, GigaSpaces, etc -->
    <!--  
    2. 配置 CacheManager. 
    2.1 需要加入 ehcache 的 jar 包及配置檔案. 
    -->     
    <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
        <!-- Set a net.sf.ehcache.CacheManager instance here if you already have one.  If not, a new one
             will be creaed with a default config:
             <property name="cacheManager" ref="ehCacheManager"/> -->
        <!-- If you don't have a pre-built net.sf.ehcache.CacheManager instance to inject, but you want
             a specific Ehcache configuration to be used, specify that here.  If you don't, a default
             will be used.: -->
        <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/> 
    </bean>
    
    <bean id="authenticator" 
    	class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
    	<property name="authenticationStrategy">
    		<bean class="org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy"></bean>
    	</property>
    </bean>

    <!-- Used by the SecurityManager to access security data (users, roles, etc).
         Many other realm implementations can be used too (PropertiesRealm,
         LdapRealm, etc. -->
    <!-- 
    	3. 配置 Realm 
    	3.1 直接配置實現了 org.apache.shiro.realm.Realm 介面的 bean
    -->     
    <bean id="jdbcRealm" class="com.atguigu.shiro.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>
    
    <bean id="secondRealm" class="com.atguigu.shiro.realms.SecondRealm">
    	<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>

    <!-- =========================================================
         Shiro Spring-specific integration
         ========================================================= -->
    <!-- Post processor that automatically invokes init() and destroy() methods
         for Spring-configured Shiro objects so you don't have to
         1) specify an init-method and destroy-method attributes for every bean
            definition and
         2) even know which Shiro objects require these methods to be
            called. -->
    <!--  
    4. 配置 LifecycleBeanPostProcessor. 可以自定的來呼叫配置在 Spring IOC 容器中 shiro bean 的生命週期方法. 
    -->       
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

    <!-- Enable Shiro Annotations for Spring-configured beans.  Only run after
         the lifecycleBeanProcessor has run: -->
    <!--  
    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>

    <!-- Define the Shiro Filter here (as a FactoryBean) instead of directly in web.xml -
         web.xml uses the DelegatingFilterProxy to access this bean.  This allows us
         to wire things with more control as well utilize nice Spring things such as
         PropertiesPlaceholderConfigurer and abstract beans or anything else we might need: -->
    <!--  
    6. 配置 ShiroFilter. 
    6.1 id 必須和 web.xml 檔案中配置的 DelegatingFilterProxy 的 <filter-name> 一致.
                      若不一致, 則會丟擲: NoSuchBeanDefinitionException. 因為 Shiro 會來 IOC 容器中查詢和 <filter-name> 名字對應的 filter bean.
    -->     
    <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>
        
        <!--  
        	配置哪些頁面需要受保護. 
        	以及訪問這些頁面需要的許可權. 
        	1). anon 可以被匿名訪問
        	2). authc 必須認證(即登入)後才可能訪問的頁面. 
        	3). logout 登出.
        	4). roles 角色過濾器
        -->
        <!--  
        <property name="filterChainDefinitions">
            <value>
                /login.jsp = anon
                /shiro/login = anon
                /shiro/logout = logout
                
                /user.jsp = roles[user]
                /admin.jsp = roles[admin]
                
                # everything else requires authentication:
                /** = authc
            </value>
        </property>
        -->
    </bean>
    
    <!-- 配置一個 bean, 該 bean 實際上是一個 Map. 通過例項工廠方法的方式 -->
    <bean id="filterChainDefinitionMap" 
    	factory-bean="filterChainDefinitionMapBuilder" factory-method="buildFilterChainDefinitionMap"></bean>
    
    <bean id="filterChainDefinitionMapBuilder"
    	class="com.atguigu.shiro.factory.FilterChainDefinitionMapBuilder"></bean>
    
    <bean id="shiroService"
    	class="com.atguigu.shiro.services.ShiroService"></bean>

</beans>

將許可權抽取出來:

package com.atguigu.shiro.factory;

import java.util.LinkedHashMap;

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;
	}
	
}

3.ehcache.xml:

<ehcache>

    <!-- Sets the path to the directory where cache .data files are created.

         If the path is a Java System Property it is replaced by
         its value in the running VM.

         The following properties are translated:
         user.home - User's home directory
         user.dir - User's current working directory
         java.io.tmpdir - Default temp file path -->
    <diskStore path="java.io.tmpdir"/>
    
    <cache name="authorizationCache"
           eternal="false"
           timeToIdleSeconds="3600"
           timeToLiveSeconds="0"
           overflowToDisk="false"
           statistics="true">
    </cache>

    <cache name="authenticationCache"
           eternal="false"
           timeToIdleSeconds="3600"
           timeToLiveSeconds="0"
           overflowToDisk="false"
           statistics="true">
    </cache>

    <cache name="shiro-activeSessionCache"
           eternal="false"
           timeToIdleSeconds="3600"
           timeToLiveSeconds="0"
           overflowToDisk="false"
           statistics="true">
    </cache>

    <!--Default Cache configuration. These will applied to caches programmatically created through
        the CacheManager.

        The following attributes are required for defaultCache:

        maxInMemory       - Sets the maximum number of objects that will be created in memory
        eternal           - Sets whether elements are eternal. If eternal,  timeouts are ignored and the element
                            is never expired.
        timeToIdleSeconds - Sets the time to idle for an element before it expires. Is only used
                            if the element is not eternal. Idle time is now - last accessed time
        timeToLiveSeconds - Sets the time to live for an element before it expires. Is only used
                            if the element is not eternal. TTL is now - creation time
        overflowToDisk    - Sets whether elements can overflow to disk when the in-memory cache
                            has reached the maxInMemory limit.

        -->
    <defaultCache
        maxElementsInMemory="10000"
        eternal="false"
        timeToIdleSeconds="120"
        timeToLiveSeconds="120"
        overflowToDisk="true"
        />

    <!--Predefined caches.  Add your cache configuration settings here.
        If you do not have a configuration for your cache a WARNING will be issued when the
        CacheManager starts

        The following attributes are required for defaultCache:

        name              - Sets the name of the cache. This is used to identify the cache. It must be unique.
        maxInMemory       - Sets the maximum number of objects that will be created in memory
        eternal           - Sets whether elements are eternal. If eternal,  timeouts are ignored and the element
                            is never expired.
        timeToIdleSeconds - Sets the time to idle for an element before it expires. Is only used
                            if the element is not eternal. Idle time is now - last accessed time
        timeToLiveSeconds - Sets the time to live for an element before it expires. Is only used
                            if the element is not eternal. TTL is now - creation time
        overflowToDisk    - Sets whether elements can overflow to disk when the in-memory cache
                            has reached the maxInMemory limit.

        -->

    <!-- Sample cache named sampleCache1
        This cache contains a maximum in memory of 10000 elements, and will expire
        an element if it is idle for more than 5 minutes and lives for more than
        10 minutes.

        If there are more than 10000 elements it will overflow to the
        disk cache, which in this configuration will go to wherever java.io.tmp is
        defined on your system. On a standard Linux system this will be /tmp"
        -->
    <cache name="sampleCache1"
        maxElementsInMemory="10000"
        eternal="false"
        timeToIdleSeconds="300"
        timeToLiveSeconds="600"
        overflowToDisk="true"
        />

    <!-- Sample cache named sampleCache2
        This cache contains 1000 elements. Elements will always be held in memory.
        They are not expired. -->
    <cache name="sampleCache2"
        maxElementsInMemory="1000"
        eternal="true"
        timeToIdleSeconds="0"
        timeToLiveSeconds="0"
        overflowToDisk="false"
        /> -->

    <!-- Place configuration for your caches following -->

</ehcache>

4.SpringMVC配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:mvc="http://www.springframework.org/schema/mvc"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
	
	<context:component-scan base-package="com.atguigu.shiro"></context:component-scan>
	
	<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<property name="prefix" value="/"></property>
		<property name="suffix" value=".jsp"></property>
	</bean>
	
	<mvc:annotation-driven></mvc:annotation-driven>
	<mvc:default-servlet-handler/>

</beans>

5.ShiroFilter工作原理:

6.Url:

6.1 配置:

格式:url=攔截器[引數],攔截器[引數]。

如果當前請求的url匹配[urls]部分的某個url,將會執行其配置的攔截器。

anon(anonymous)攔截器表示匿名訪問(即不需要登入即可訪問)。

authc(authentication)攔截器表示需要身份認證通過後才能訪問。

6.2 匹配模式:

url匹配模式使用Ant風格模式。

Ant路徑萬用字元支援?、*、**,不包括目錄分隔符/。

-?:匹配一個字元,如/admin?將匹配/admin1但是不匹配/admin和/admin/;

-*:匹配零個或多個字元,如/admin將匹配/admin、/admin123但不匹配/admin/1;

-**:匹配路徑中的零個或多個路徑,如/admin/**將匹配/admin/a、/admin/a/b;

6.3 匹配順序:

URL許可權採取第一次匹配優先的方式,即從頭開始使用第一個匹配的URL模式對應的攔截器鏈。

如:路徑是/bb/aa;

url配置是:/bb/**=filter1和/bb/aa=filter2;

則會使用filter1進行攔截。

7.認證過程:

1. 獲取當前的 Subject. 呼叫 SecurityUtils.getSubject();

2. 測試當前的使用者是否已經被認證. 即是否已經登入. 呼叫 Subject 的 isAuthenticated() ;

3. 若沒有被認證, 則把使用者名稱和密碼封裝為 UsernamePasswordToken 物件;

1). 建立一個表單頁面。

2). 把請求提交到 SpringMVC 的 Handler。

3). 獲取使用者名稱和密碼。

4. 執行登入: 呼叫 Subject 的 login(AuthenticationToken) 方法;

5. 自定義 Realm 的方法, 從資料庫中獲取對應的記錄, 返回給 Shiro;

1). 實際上需要繼承 org.apache.shiro.realm.AuthenticatingRealm 類。

2). 實現 doGetAuthenticationInfo(AuthenticationToken) 方法。

6. 由 shiro 完成對密碼的比對;

通過 AuthenticatingRealm 的 credentialsMatcher 屬性來進行的密碼的比對!

7. 把一個字串加密為 MD5;

替換當前 Realm 的 credentialsMatcher 屬性. 直接使用 HashedCredentialsMatcher 物件, 並設定加密演算法即可。

8. 使用 MD5 鹽值加密;

1). 在 doGetAuthenticationInfo 方法返回值建立 SimpleAuthenticationInfo 物件的時候, 需要使用SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName) 構造器。

2). 使用 ByteSource.Util.bytes() 來計算鹽值。

3). 鹽值需要唯一:一般使用隨機字串或 user id。

4). 使用 new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations); 來計算鹽值加密後的密碼的值。

Controller:

package com.atguigu.shiro.handlers;

import javax.servlet.http.HttpSession;

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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import com.atguigu.shiro.services.ShiroService;

@Controller
@RequestMapping("/shiro")
public class ShiroHandler {
	
	@Autowired
	private ShiroService shiroService;
	
	@RequestMapping("/testShiroAnnotation")
	public String testShiroAnnotation(HttpSession session){
		session.setAttribute("key", "value12345");
		shiroService.testMethod();
		return "redirect:/list.jsp";
	}

	@RequestMapping("/login")
	public String login(@RequestParam("username") String username, 
			@RequestParam("password") String password){
		Subject currentUser = SecurityUtils.getSubject();
		
		if (!currentUser.isAuthenticated()) {
            UsernamePasswordToken token = new UsernamePasswordToken(username, password);
            // rememberme
            token.setRememberMe(true);
            try {
            	System.out.println("1. " + token.hashCode());
                currentUser.login(token);
            } 
            // ... catch more exceptions here (maybe custom ones specific to your application?
            catch (AuthenticationException ae) {
                //unexpected condition?  error?
            	System.out.println(ae.getMessage());
            }
        }
		
		return "redirect:/list.jsp";
	}
	
}

Realm:

package com.atguigu.shiro.realms;

import java.util.HashSet;
import java.util.Set;

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.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;

public class ShiroRealm extends AuthorizingRealm {

	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(
			AuthenticationToken token) throws AuthenticationException {
		System.out.println("[FirstRealm] doGetAuthenticationInfo");
		
        //轉UsernamePasswordToken
		UsernamePasswordToken upToken = (UsernamePasswordToken) token;
		
        //得到username
		String username = upToken.getUsername();
		
        //從資料庫中得到username
		System.out.println("資料庫中的username: " + username);
		
        //若使用者不存在丟擲異常
		if("unknown".equals(username)){
			throw new UnknownAccountException(“使用者不存在”);
		}
		
        //根據使用者資訊丟擲其他異常
		if("monster".equals(username)){
			throw new LockedAccountException(“使用者被鎖定”);
		}
		
        //根據使用者情況來構建AuthenticationInfo物件並返回,通常使用的實現類是SimpleAuthenticationInfo
        //principal:認證的實體資訊,可能是username也可能是資料庫對應的使用者實體類物件
		Object principal = username;
        //credentials:密碼
		Object credentials = null; 
		if("admin".equals(username)){
			credentials = "038bdaf98f2037b31f1e75b5b4c9b26e";
		}else if("user".equals(username)){
			credentials = "098d2c478e9c11555ce2823231e02ec1";
		}
		
        //realmName:當前realm物件的name,呼叫父類的getName()方法即可
		String realmName = getName();
        //鹽值
		ByteSource credentialsSalt = ByteSource.Util.bytes(username);
		
		SimpleAuthenticationInfo info = null; //new SimpleAuthenticationInfo(principal, credentials, realmName);
		info = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
		return info;
	}

	public static void main(String[] args) {
		String hashAlgorithmName = "MD5";
		Object credentials = "123456";
		Object salt = ByteSource.Util.bytes("user");;
		int hashIterations = 1024;
		
		Object result = new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
		System.out.println(result);
	}

    //授權時呼叫
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(
			PrincipalCollection principals) {
        //從PrincipalCollection獲取登入使用者的資訊
		Object principal = principals.getPrimaryPrincipal();
		
        //利用登入的使用者資訊來獲取使用者當前的角色和許可權(可能需要查詢資料庫)
		Set<String> roles = new HashSet<>();
		roles.add("user");
		if("admin".equals(principal)){
			roles.add("admin");
		}
		
        //建立SimpleAuthorizationInfo,設定roles屬性
		SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
		
		return info;
	}
}

8.多Realm:

secondRealm:

package com.atguigu.shiro.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 SecondRealm extends AuthenticatingRealm {

	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(
			AuthenticationToken token) throws AuthenticationException {
		System.out.println("[SecondReaml] doGetAuthenticationInfo");
		 
		UsernamePasswordToken upToken = (UsernamePasswordToken) token;
		
		String username = upToken.getUsername();
		
		System.out.println("資料庫中的username: " + username);
		
		if("unknown".equals(username)){
			throw new UnknownAccountException(“使用者不存在”);
		}
		
		if("monster".equals(username)){
			throw new LockedAccountException(“使用者被鎖定”);
		}
		
		Object principal = username;
		Object credentials = null; //"fc1709d0a95a6be30bc5926fdb7f22f4";
		if("admin".equals(username)){
			credentials = "ce2f6417c7e1d32c1d81a797ee0b499f87c5de06";
		}else if("user".equals(username)){
			credentials = "073d4c3ae812935f23cb3f2a71943f49e082a718";
		}
		
		String realmName = getName();
		ByteSource credentialsSalt = ByteSource.Util.bytes(username);
		
		SimpleAuthenticationInfo info = null; //new SimpleAuthenticationInfo(principal, credentials, realmName);
		info = new SimpleAuthenticationInfo("secondRealmName", credentials, credentialsSalt, realmName);
		return info;
	}

	public static void main(String[] args) {
		String hashAlgorithmName = "SHA1";
		Object credentials = "123456";
		Object salt = ByteSource.Util.bytes("admin");;
		int hashIterations = 1024;
		
		Object result = new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
		System.out.println(result);
	}
}

8.1 多Realm認證策略(AuthenticationStrategy):

AuthenticationStrategy 介面的預設實現:

FirstSuccessfulStrategy:只要有一個 Realm 驗證成功即可,只返回第 一個 Realm 身份驗證成功的認證資訊,其他的忽略;

AtLeastOneSuccessfulStrategy:只要有一個Realm驗證成功即可,和FirstSuccessfulStrategy 不同,將返回所有Realm身份驗證成功的認證資訊;

AllSuccessfulStrategy:所有Realm驗證成功才算成功,且返回所有Realm身份驗證成功的認證資訊,如果有一個失敗就失敗了。

ModularRealmAuthenticator 預設是 AtLeastOneSuccessfulStrategy策略。

9.授權:

授權,也叫訪問控制,即在應用中控制誰訪問哪些資源(如訪問頁面/編輯資料/頁面操作 等)。在授權中需瞭解的幾個關鍵物件:主體(Subject)、資源(Resource)、許可權 (Permission)、角色(Role)。

主體(Subject):訪問應用的使用者,在 Shiro 中使用 Subject 代表該使用者。使用者只有授權 後才允許訪問相應的資源。

資源(Resource):在應用中使用者可以訪問的 URL,比如訪問 JSP 頁面、檢視/編輯某些 資料、訪問某個業務方法、列印文字等等都是資源。使用者只要授權後才能訪問。

許可權(Permission):安全策略中的原子授權單位,通過許可權我們可以表示在應用中使用者 有沒有操作某個資源的權力。即許可權表示在應用中使用者能不能訪問某個資源,如:訪問用 戶列表頁面檢視/新增/修改/刪除使用者資料(即很多時候都是CRUD(增查改刪)式許可權控 制)等。許可權代表了使用者有沒有操作某個資源的權利,即反映在某個資源上的操作允不允 許。

Shiro 支援粗粒度許可權(如使用者模組的所有許可權)和細粒度許可權(操作某個使用者的許可權, 即例項級別的)

角色(Role):許可權的集合,一般情況下會賦予使用者角色而不是許可權,即這樣使用者可以擁有 一組許可權,賦予許可權時比較方便。典型的如:專案經理、技術總監、CTO、開發工程師等 都是角色,不同的角色擁有一組不同的許可權。

9.1 授權方式:

Shiro 支援三種方式的授權:

– 程式設計式:通過寫if/else 授權程式碼塊完成

– 註解式:通過在執行的Java方法上放置相應的註解完成,沒有許可權將丟擲相 應的異常

– JSP/GSP 標籤:在JSP/GSP 頁面通過相應的標籤完成

9.2 預設攔截器:

Shiro 內建了很多預設的攔截器,比如身份驗證、授權等 相關的。預設攔截器可以參考org.apache.shiro.web.filter.mgt.DefaultFilter中的列舉攔截器:

身份驗證相關的過濾器:

授權相關過濾器:

其他:

1. 授權需要繼承 AuthorizingRealm 類, 並實現其 doGetAuthorizationInfo 方法。

2. AuthorizingRealm 類繼承自 AuthenticatingRealm, 但沒有實現 AuthenticatingRealm 中的doGetAuthenticationInfo, 所以認證和授權只需要繼承 AuthorizingRealm 就可以了. 同時實現他的兩個抽象方法。

package com.atguigu.shiro.realms;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

public class TestRealm extends AuthorizingRealm {

	//用於授權 
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(
			PrincipalCollection principals) {
		// TODO Auto-generated method stub
		return null;
	}

	//用於認證
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(
			AuthenticationToken token) throws AuthenticationException {
		// TODO Auto-generated method stub
		return null;
	}

}

9.3 許可權(Permissions):

規則:資源識別符號:操作:物件例項 ID 即對哪個資源的哪個 例項可以進行什麼操作。其預設支援萬用字元許可權字串,“:”表示資源/操作/例項的分割;“,”表示操作的分割,“*” 表示任意資 源/操作/例項。

多層次管理:

– 例如:user:query、user:edit

– 冒號是一個特殊字元,它用來分隔許可權字串的下一部件:第一部分 是許可權被操作的領域(印表機),第二部分是被執行的操作

– 多個值:每個部件能夠保護多個值。因此,除了授予使用者user:query和 user:edit 許可權外,也可以簡單地授予他們一個:user:query, edit

– 還可以用 * 號代替所有的值,如:user:* , 也可以寫:*:query,表示 某個使用者在所有的領域都有 query 的許可權

Shiro的Permissions:

例項級訪問控制

– 這種情況通常會使用三個部:域、操作、被付諸實施的例項。如:user:edit:manager

– 也可以使用萬用字元來定義,如:user:edit:*user:*:*user:*:manager

– 部分省略萬用字元:缺少的部件意味著使用者可以訪問所有與之匹配的值,比如:user:edit 等價於 user:edit :*user 等價於 user:*:*

– 注意:萬用字元只能從字串的結尾處省略部件,也就 是說 user:edit 並不等價於 user:*:edit

9.4 授權流程:

1、首先呼叫 Subject.isPermitted*/hasRole* 介面,其會委託給

SecurityManager,而 SecurityManager 接著會委託給 Authorizer;

2、Authorizer是真正的授權者,如果呼叫如isPermitted(“user:view”),其首先會通過

PermissionResolver 把字串轉換成相應的 Permission 例項;

3、在進行授權之前,其會呼叫相應的 Realm 獲取 Subject 相應的角

色/許可權用於匹配傳入的角色/許可權;

4、Authorizer 會判斷 Realm 的角色/許可權是否和傳入的匹配,如果 有多個Realm,會委託給 ModularRealmAuthorizer 進行迴圈判斷, 如果匹配如 isPermitted*/hasRole* 會返回true,否則返回false表示 授權失敗。

ModularRealmAuthorizer 進行多 Realm 匹配流程:

– 1、首先檢查相應的 Realm 是否實現了實現了Authorizer;

– 2、如果實現了 Authorizer,那麼接著呼叫其相應的isPermitted*/hasRole* 介面進行匹配;

– 3、如果有一個Realm匹配那麼將返回 true,否則返回 false。

10.Shiro標籤:

Shiro 提供了 JSTL 標籤用於在 JSP 頁面進行許可權控制,如根據登入使用者顯示相應的頁面按鈕。

guest 標籤:使用者沒有身份驗證時顯示相應資訊,即遊客訪問資訊:

user 標籤:使用者已經經過認證/記住我登入後顯示相應的資訊。

authenticated 標籤:使用者已經身份驗證通過,即Subject.login登入成功,不是記住我登入的

notAuthenticated 標籤:使用者未進行身份驗證,即沒有呼叫Subject.login進行登入,包括記住我自動登入的也屬於未進行身份驗證。

pincipal 標籤:顯示使用者身份資訊,預設呼叫Subject.getPrincipal() 獲取,即 Primary Principal。

hasRole 標籤:如果當前 Subject 有角色將顯示 body 體內容:

hasAnyRoles 標籤:如果當前Subject有任意一個角色(或的關係)將顯示body體內容。

lacksRole:如果當前 Subject 沒有角色將顯示 body 體內容

hasPermission:如果當前 Subject 有許可權將顯示 body 體內容

lacksPermission:如果當前Subject沒有許可權將顯示body體內容。

11.許可權註解:

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

@RequiresUser:表示當前 Subject 已經身份驗證或者通過記住我登入的。

@RequiresGuest:表示當前Subject沒有身份驗證或通過記住我登入過,即是遊客身份。

@RequiresRoles(value={“admin”, “user”}, logical=Logical.AND):表示當前 Subject 需要角色 admin 和user

@RequiresPermissions (value={“user:a”, “user:b”},logical= Logical.OR):表示當前 Subject 需要許可權 user:a 或user:b。

可以用在Service:

package com.atguigu.shiro.services;

import java.util.Date;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.session.Session;

public class ShiroService {
	
	@RequiresRoles({"admin"})
	public void testMethod(){
		System.out.println("testMethod, time: " + new Date());
		
		Session session = SecurityUtils.getSubject().getSession();
		Object val = session.getAttribute("key");
		
		System.out.println("Service SessionVal: " + val);
	}
	
}

但是一般在專案中,Service上面會有事務註解,這個時候就不能夠使用Shiro註解了,因為這個時候這個Service已經是一個代理物件了,如果在進行註解就會是一個代理的代理,就會出錯。這個時候就需要將註解用在Web層上。

12.會話(Session):

Shiro 提供了完整的企業級會話管理功能,不依賴於底層容 器(如web容器tomcat),不管 JavaSE 還是 JavaEE 環境 都可以使用,提供了會話管理、會話事件監聽、會話儲存/持久化、容器無關的叢集、失效/過期支援、對Web 的透明 支援、SSO 單點登入的支援等特性。

12.1 相關API:

Subject.getSession():即可獲取會話;其等價於Subject.getSession(true),即如果當前沒有建立 Session 物件會建立 一個;Subject.getSession(false),如果當前沒有建立 Session 則返回null

session.getId():獲取當前會話的唯一標識

session.getHost():獲取當前Subject的主機地址

session.getTimeout() & session.setTimeout(毫秒):獲取/設定當 前Session的過期時間

session.getStartTimestamp() & session.getLastAccessTime():獲取會話的啟動時間及最後訪問時間;如果是 JavaSE 應用需要自己定期呼叫 session.touch() 去更新最後訪問時間;如果是 Web 應用,每 次進入 ShiroFilter 都會自動呼叫 session.touch() 來更新最後訪問時間。

session.touch() & session.stop():更新會話最後訪問時 間及銷燬會話;當Subject.logout()時會自動呼叫 stop 方法來銷燬會話。如果在web中,呼叫 HttpSession. invalidate()也會自動呼叫Shiro Session.stop 方法進行銷燬Shiro 的會 話

session.setAttribute(key, val) & session.getAttribute(key) & session.removeAttribute(key):設定/獲取/刪除會話屬性;在整個會話範圍內都可以對這些屬性進行操作

12.2 會話監聽器:

會話監聽器用於監聽會話建立、過期及停止事件

12.3 SessionDao:

AbstractSessionDAO 提供了 SessionDAO 的基礎實現, 如生成會話ID等

CachingSessionDAO 提供了對開發者透明的會話快取的 功能,需要設定相應的 CacheManager

MemorySessionDAO 直接在記憶體中進行會話維護

EnterpriseCacheSessionDAO 提供了快取功能的會話維 護,預設情況下使用 MapCache 實現,內部使用ConcurrentHashMap 儲存快取的會話。

配置SessionDao:

配置快取:

建立Session資料表:

create table sessions (
id varchar(200),
session varchar(2000),
constraint pk_sessions primary key(id)
) charset=utf8 ENGINE=InnoDB;

實現:

package com.atguigu.shiro.realms;

import java.io.Serializable;
import java.util.List;

import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.ValidatingSession;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;

public class MySessionDao extends EnterpriseCacheSessionDAO {

	@Autowired
	private JdbcTemplate jdbcTemplate = null;

	@Override
	protected Serializable doCreate(Session session) {
		Serializable sessionId = generateSessionId(session);
		assignSessionId(session, sessionId);
		String sql = "insert into sessions(id, session) values(?,?)";
		jdbcTemplate.update(sql, sessionId,
				SerializableUtils.serialize(session));
		return session.getId();
	}

	@Override
	protected Session doReadSession(Serializable sessionId) {
		String sql = "select session from sessions where id=?";
		List<String> sessionStrList = jdbcTemplate.queryForList(sql,
				String.class, sessionId);
		if (sessionStrList.size() == 0)
			return null;
		return SerializableUtils.deserialize(sessionStrList.get(0));
	}
	
	@Override
	protected void doUpdate(Session session) {
		if (session instanceof ValidatingSession
				&& !((ValidatingSession) session).isValid()) {
			return; 
		}
		String sql = "update sessions set session=? where id=?";
		jdbcTemplate.update(sql, SerializableUtils.serialize(session),
				session.getId());
	}

	@Override
	protected void doDelete(Session session) {
		String sql = "delete from sessions where id=?";
		jdbcTemplate.update(sql, session.getId());
	}
}

序列化工具類:

package com.atguigu.shiro.realms;

import org.apache.shiro.codec.Base64;
import org.apache.shiro.session.Session;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class SerializableUtils {

	public static String serialize(Session session) {
		try {
			ByteArrayOutputStream bos = new ByteArrayOutputStream();
			ObjectOutputStream oos = new ObjectOutputStream(bos);
			oos.writeObject(session);
			return Base64.encodeToString(bos.toByteArray());
		} catch (Exception e) {
			throw new RuntimeException("serialize session error", e);
		}
	}

	public static Session deserialize(String sessionStr) {
		try {
			ByteArrayInputStream bis = new ByteArrayInputStream(
					Base64.decode(sessionStr));
			ObjectInputStream ois = new ObjectInputStream(bis);
			return (Session) ois.readObject();
		} catch (Exception e) {
			throw new RuntimeException("deserialize session error", e);
		}
	}
	
}

12.4 會話驗證:

Shiro 提供了會話驗證排程器,用於定期的驗證會話是否

已過期,如果過期將停止會話

出於效能考慮,一般情況下都是獲取會話時來驗證會話是 否過期並停止會話的;但是如在 web 環境中,如果使用者不 主動退出是不知道會話是否過期的,因此需要定期的檢測 會話是否過期,Shiro 提供了會話驗證排程器SessionValidationScheduler

Shiro 也提供了使用Quartz會話驗證排程器:QuartzSessionValidationScheduler

13.快取:

13.1 CacheManagerAware介面:

Shiro 內部相應的元件(DefaultSecurityManager)會自 動檢測相應的物件(如Realm)是否實現了CacheManagerAware 並自動注入相應的CacheManager。

13.2 Realm快取:

Shiro 提供了 CachingRealm,其實現了CacheManagerAware 介面,提供了快取的一些基礎實現;

AuthenticatingRealm 及 AuthorizingRealm 也分別提供了對AuthenticationInfo 和 AuthorizationInfo 資訊的快取。

13.3 Session快取:

如 SecurityManager 實現了 SessionSecurityManager, 其會判斷 SessionManager 是否實現了CacheManagerAware 介面,如果實現了會把CacheManager 設定給它。

SessionManager 也會判斷相應的 SessionDAO(如繼承 自CachingSessionDAO)是否實現了CacheManagerAware,如果實現了會把 CacheManager設定給它

設定了快取的 SessionManager,查詢時會先查快取,如 果找不到才查資料庫。

14.Remember Me:

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

1、首先在登入頁面選中 RememberMe 然後登入成功;如果是 瀏覽器登入,一般會把 RememberMe 的Cookie 寫到客戶端並 儲存下來;

2、關閉瀏覽器再重新開啟;會發現瀏覽器還是記住你的;

3、訪問一般的網頁伺服器端還是知道你是誰,且能正常訪問;

4、但是比如我們訪問淘寶時,如果要檢視我的訂單或進行支付 時,此時還是需要再進行身份認證的,以確保當前使用者還是你。

14.1 認證和記住我:

subject.isAuthenticated() 表示使用者進行了身份驗證登入的,

即使有 Subject.login 進行了登入;

• subject.isRemembered():表示使用者是通過記住我登入的, 此時可能並不是真正的你(如你的朋友使用你的電腦,或者 你的cookie 被竊取)在訪問的

• 兩者二選一,即 subject.isAuthenticated()==true,則subject.isRemembered()==false;反之一樣。

訪問一般網頁:如個人在主頁之類的,我們使用user 攔截 器即可,user 攔截器只要使用者登入

(isRemembered() || isAuthenticated())過即可訪問成功;

• 訪問特殊網頁:如我的訂單,提交訂單頁面,我們使用authc 攔截器即可,authc 攔截器會判斷使用者是否是通過Subject.login(isAuthenticated()==true)登入的,如 果是才放行,否則會跳轉到登入頁面叫你重新登入。