1. 程式人生 > >Shiro - 會話管理與SessionDao

Shiro - 會話管理與SessionDao

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

【1】Shiro Session介面與實現類

這裡的Session不再是我們通常使用的javax.servlet.http.HttpSession,而是org.apache.shiro.session.Session。

一個Session是與一段時間內與軟體系統互動的單個物件(使用者、守護程序等)相關的有狀態資料上下文。

該Session旨在由業務層管理,並且可以通過其他層訪問,而不繫結到任何給定的客戶端技術。這是一個很大的好處對Java系統而言,因為到目前為止,唯一可行的會話機制是{javax .servlet .http.httpsession }或有狀態會話EJB,這些應用程式多次不必要地將應用程式耦合到Web或EJB技術。

不過在使用上與httpsession 有相似之處,相關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() 也會自動呼叫ShiroSession.stop方法進行銷燬Shiro的會話
  • session.setAttribute(key, val) & session.getAttribute(key) & session.removeAttribute(key):設定/獲取/刪除會話屬性;在整個會話範圍內都可以對這些屬性進行操作。

Session實現類如下
在這裡插入圖片描述


HttpSession實現類如下:

在這裡插入圖片描述


SessionManager實現類如下:

在這裡插入圖片描述


【2】會話監聽器

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

原始碼如下:

public interface SessionListener {

    /**
     * Notification callback that occurs when the corresponding Session has started.
     *
     * @param session the session that has started.
     */
    void onStart(Session session);

    /**
     * Notification callback that occurs when the corresponding Session has stopped, either programmatically via
     * {@link Session#stop} or automatically upon a subject logging out.
     *
     * @param session the session that has stopped.
     */
    void onStop(Session session);

    /**
     * Notification callback that occurs when the corresponding Session has expired.
     * <p/>
     * <b>Note</b>: this method is almost never called at the exact instant that the {@code Session} expires.  Almost all
     * session management systems, including Shiro's implementations, lazily validate sessions - either when they
     * are accessed or during a regular validation interval.  It would be too resource intensive to monitor every
     * single session instance to know the exact instant it expires.
     * <p/>
     * If you need to perform time-based logic when a session expires, it is best to write it based on the
     * session's {@link org.apache.shiro.session.Session#getLastAccessTime() lastAccessTime} and <em>not</em> the time
     * when this method is called.
     *
     * @param session the session that has expired.
     */
    void onExpiration(Session session);
}


Shiro Session一個重要應用

在Controller通常會使用HttpSession進行操作,那麼在Service層為了降低侵入、解耦,我們就可以使用Shiro Session進行操作。

如在Controller放入Session中一個鍵值對:

	 @ResponseBody
    @RequestMapping(value="/test",produces="application/json;charset=utf-8")
    public String  test(HttpSession session) {
    	System.out.println("呼叫方法test");
    	session.setAttribute("key", "123456");
    	return "success";
    }

在Service使用Shiro Session進行獲取:

	@Override
	public List<SysRole> getRoleListByUserId(Long id) {
		// TODO Auto-generated method stub
		Session session = SecurityUtils.getSubject().getSession();
		Object attribute = session.getAttribute("key");
		List<SysRole> roleListByUserId = userServiceDao.getRoleListByUserId(id);
		return roleListByUserId;
	}

【3】SessionDao

SessionDao提供了一種方式,使我們能夠將session存入資料庫(快取中)中進行CRUD操作。這有什麼意義?當只有一臺伺服器一個專案的時候通常你不必管理Session,Shiro會自行管理Session。

但是如果有多個伺服器同時跑一個專案呢?或者單點登入,不同專案在不同伺服器,但是需要實現單點登入功能。這是你就需要在伺服器之間共享Session!專案中通常我們使用Redis來實現共享Session。

① SessionDao介面繼承圖如下:

在這裡插入圖片描述


② 幾個實現類

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

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

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

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


③ xml配置與自定義MySessionDao

pom檔案中關於Shiro依賴如下:

 <!-- shiro 版本為1.4.0 -->
    <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-core</artifactId>
      <version>${shiro.version}</version>
    </dependency>
    <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-ehcache</artifactId>
      <version>${shiro.version}</version>
    </dependency>
   
    <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-web</artifactId>
      <version>${shiro.version}</version>
    </dependency>
    <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-spring</artifactId>
      <version>${shiro.version}</version>
    </dependency>
    <dependency> 
	    <groupId>org.apache.shiro</groupId>
	    <artifactId>shiro-quartz</artifactId> 
	    <version>${shiro.version}</version>
	</dependency>

自定義MySessionDao:

public class MySessionDao extends EnterpriseCacheSessionDAO {

	//這裡注入Spring提供的JdbcTemplate
	@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());
	}
}

Shiro XML配置如下:

 <!-- 配置需要向Cookie中儲存資料的配置模版 --> 
	<bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie"> 
	    <!-- 在Tomcat執行下預設使用的Cookie的名字為JSESSIONID --> 
	    <constructor-arg value="shiro-session-id"/> 
	    <!-- 保證該系統不會受到跨域的指令碼操作供給 --> 
	    <property name="httpOnly" value="true"/> 
	    <!-- 定義Cookie的過期時間,單位為秒,如果設定為-1表示瀏覽器關閉,則Cookie消失 --> 
	    <property name="maxAge" value="-1"/> 
	</bean>

    <!-- Session ID 生成器-->
	<bean id="sessionIdGenerator"
		class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator"/>
	
	<!-- Session DAO. 繼承 EnterpriseCacheSessionDAO -->
	<bean id="sessionDAO"
		class="com.web.maven.shiro.MySessionDao">
		<property name="activeSessionsCacheName" value="shiro-activeSessionCache"/>
		<property name="sessionIdGenerator" ref="sessionIdGenerator"/>
	</bean>
	
	<!-- 會話管理器-->
	<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
	 <!-- 定義的是全域性的session會話超時時間,此操作會覆蓋web.xml檔案中的超時時間配置 -->
		<property name="globalSessionTimeout" value="1800000"/>
		<!-- 刪除所有無效的Session物件,此時的session被儲存在了記憶體裡面 -->
		<property name="deleteInvalidSessions" value="true"/>
		 <!-- 定義要使用的無效的Session定時排程器 -->
        <property name="sessionValidationScheduler" ref="sessionValidationScheduler"/>
         <!-- 需要讓此session可以使用該定時排程器進行檢測 -->
        <property name="sessionValidationSchedulerEnabled" value="true"/>
        <!-- 定義Session可以進行操作的DAO -->
		<property name="sessionDAO" ref="sessionDAO"/>
		<!-- 所有的session一定要將id設定到Cookie之中,需要提供有Cookie的操作模版 -->
        <property name="sessionIdCookie" ref="sessionIdCookie"/>
         <!-- 定義sessionIdCookie模版可以進行操作的啟用 -->
        <property name="sessionIdCookieEnabled" value="true"/>
        <!-- url sessionId  重寫 -->
        <property name="sessionIdUrlRewritingEnabled" value="true"/>
	</bean>
	
	<!-- 配置session的定時驗證檢測程式類,以讓無效的session釋放 -->
    <bean id="sessionValidationScheduler"
        class="org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler">
        <!-- 設定session的失效掃描間隔,單位為毫秒 -->
        <property name="sessionValidationInterval" value="100000"/>
        <property name="sessionManager" ref="sessionManager" />
    </bean> 

	
    <!-- 安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <!-- 注入自定義Realm -->
<!--         <property name="realm" ref="customRealm"/> -->
        <!-- 注入快取管理器 -->
        <property name="cacheManager" ref="cacheManager"/>
        <property name="authenticator" ref="authenticator" />
        <property name="realms">
        	<list>
    			<ref bean="customRealm"/>
<!--     			<ref bean="customRealm2"/> -->
    		</list>
        </property>
        <property name="sessionManager" ref="sessionManager" />
    </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>

    <!-- 快取管理器 -->
    <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
        <property name="cacheManagerConfigFile" value="classpath:shiro-ehcache.xml"/>
    </bean>
    
      <!-- 自定義Realm -->
    <bean id="customRealm" class="com.web.maven.shiro.CustomRealm">
        <!-- 將憑證匹配器設定到realm中,realm按照憑證匹配器的要求進行雜湊 -->
        <property name="credentialsMatcher">
           <bean  class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
              <property name="hashAlgorithmName" value="MD5"/>
              <property name="hashIterations" value="1"/>
            </bean>
        </property>
    </bean>
      <!-- 自定義SecondRealm -->
    <bean id="customRealm2" class="com.web.maven.shiro.CustomRealm2">
        <!-- 將憑證匹配器設定到realm中,realm按照憑證匹配器的要求進行雜湊 -->
        <property name="credentialsMatcher">
           <bean  class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
              <property name="hashAlgorithmName" value="SHA1"/>
              <property name="hashIterations" value="1"/>
            </bean>
        </property>
    </bean>
    
    <!-- 配置lifecycleBeanPostProcessor,可以自動的呼叫配置在spring IOC 容器中shiro bean的生命週期方法。 -->
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"></bean>

	 <!-- 開啟Shiro的註解,實現對Controller的方法級許可權檢查(如@RequiresRoles,@RequiresPermissions),
	 		需藉助SpringAOP掃描使用Shiro註解的類,並在必要時進行安全邏輯驗證  -->   
    <!-- Enable Shiro Annotations for Spring-configured beans. Only run after the lifecycleBeanProcessor has run -->  
	<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>
	
    <!-- Shiro過濾器 -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <!-- Shiro的核心安全介面,這個屬性是必須的 -->
        <property name="securityManager" ref="securityManager"/>
        <!-- loginUrl認證提交地址,如果沒有認證將會請求此地址進行認證,請求此地址將由formAuthenticationFilter進行表單認證 -->
        <property name="loginUrl" value="/login"/>
        <!-- if is Authenticated,then ,rediret to the url  -->
        <property name="successUrl" value="/index"/>
        <!-- has no permission and then redirect to the url  -->
        <property name="unauthorizedUrl" value="/refuse"></property>
       <!--<property name="filters">
            <map>
               		重寫 退出過濾器
                <entry key="logout" value-ref="systemLogoutFilter" />
            </map>
        </property>-->
        <!-- Shiro連線約束配置,即過濾鏈的定義 -->
        <property name="filterChainDefinitions">
            <value>
                <!-- /** = anon所有url都可以匿名訪問 -->
                <!-- 對靜態資源設定匿名訪問 -->
                /test=anon
                /favicon.ico = anon
                /images/** = anon
                /js/** = anon
                /styles/** = anon
                /css/** = anon
                /*.jar = anon
                <!-- 驗證碼,可匿名訪問 -->
                /validateCode = anon  
                /login = anon
                /doLogin = anon
                <!--請求logout,shrio擦除sssion-->
                /logout=logout
                <!-- /** = authc 所有url都必須認證通過才可以訪問 -->
                /**=authc
            </value>
        </property>
<!--         <property name="filterChainDefinitionMap" ref="filterChainDefinitionMap" /> -->
    </bean>
     <!-- 配置一個 bean, 該 bean 實際上是一個 Map. 通過例項工廠方法的方式 -->
<!--     <bean id="filterChainDefinitionMap"  -->
<!--     	factory-bean="filterChainDefinitionMapBuilder" factory-method="buildFilterChainDefinitionMap"></bean> -->
    
<!--     <bean id="filterChainDefinitionMapBuilder" -->
<!--     	class="com.web.maven.factory.FilterChainDefinitionMapBuilder"></bean> -->

shiro-ehcache.xml中配置快取如下:

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

資料表sessions建立語句如下:

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

【4】會話驗證

Shiro提供了會話驗證排程器,用於定期的驗證會話是否已過期,如果過期將停止會話。

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

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

具體配置參考【3】。