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】。