目前安全框架shiro使用較爲普遍,其功能也比較強大。爲了分佈式session共享,一般的作法是將session存儲在redis中,實現多個節點獲取同一個session。此實現能夠實現session共享,但session的特色是內存存儲,就是爲了高速頻繁訪問,每一個請求都必須驗證session是否存在是否過時,也從session中獲取數據。這樣致使一個頁面刷新過程當中的數十個請求會同時訪問redis,在幾毫秒內同時操做session的獲取,修改,更新,保存,刪除等操做,從而形成redis的併發量飆升,刷新一個頁面操做redis幾十到幾百次。java
爲了解決因爲session共享形成的redis高併發問題,很明顯須要在redis以前作一次短暫的session緩存,若是該緩存存在就不用從redis中獲取,從而減小同時訪問redis的次數。若是作session緩存,主要有兩種種方案,其實原理都相同:jquery
1>重寫sessionManager的retrieveSession方法。首先從request中獲取session,若是request中不存在再走原來的從redis中獲取。這樣可讓一個請求的屢次訪問redis問題獲得解決,由於request的生命週期爲瀏覽器發送一個請求到接收服務器的一次響應完成,所以,在一次請求中,request中的session是一直存在的,而且不用擔憂session超時過時等的問題。這樣就能夠達到有多少次請求就幾乎有多少次訪問redis,大大減小單次請求,頻繁訪問redis的問題。大大減小redis的併發數量。此實現方法最爲簡單。web
1 package cn.uce.web.login.filter; 2 3 import java.io.Serializable; 4 5 import javax.servlet.ServletRequest; 6 7 import org.apache.shiro.session.Session; 8 import org.apache.shiro.session.UnknownSessionException; 9 import org.apache.shiro.session.mgt.SessionKey; 10 import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; 11 import org.apache.shiro.web.session.mgt.WebSessionKey; 12 13 public class ShiroSessionManager extends DefaultWebSessionManager { 14 /** 15 * 獲取session 16 * 優化單次請求須要屢次訪問redis的問題 17 * @param sessionKey 18 * @return 19 * @throws UnknownSessionException 20 */ 21 @Override 22 protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException { 23 Serializable sessionId = getSessionId(sessionKey); 24 25 ServletRequest request = null; 26 if (sessionKey instanceof WebSessionKey) { 27 request = ((WebSessionKey) sessionKey).getServletRequest(); 28 } 29 30 if (request != null && null != sessionId) { 31 Object sessionObj = request.getAttribute(sessionId.toString()); 32 if (sessionObj != null) { 33 return (Session) sessionObj; 34 } 35 } 36 37 Session session = super.retrieveSession(sessionKey); 38 if (request != null && null != sessionId) { 39 request.setAttribute(sessionId.toString(), session); 40 } 41 return session; 42 } 43 }
<!-- session管理器 --> <bean id="sessionManager" class="cn.uce.web.login.filter.ShiroSessionManager"> <!-- 超時時間 --> <property name="globalSessionTimeout" value="${session.global.timeout}" /> <!-- session存儲的實現 --> <property name="sessionDAO" ref="redisSessionDAO" /> <!-- <property name="deleteInvalidSessions" value="true"/> --> <!-- 定時檢查失效的session --> <!-- <property name="sessionValidationSchedulerEnabled" value="true" /> --> <!-- <property name="sessionValidationScheduler" ref="sessionValidationScheduler"/> <property name="sessionIdCookieEnabled" value="true"/> --> <property name="sessionIdCookie" ref="sessionIdCookie" /> </bean>
2>session緩存於本地內存中。自定義cacheRedisSessionDao,該sessionDao中一方面注入cacheManager用於session緩存,另外一方面注入redisManager用於session存儲,當createSession和updateSession直接使用redisManager操做redis.保存session.當readSession先用cacheManager從cache中讀取,若是不存在再用redisManager從redis中讀取。注意:該方法最大的特色是session緩存的存活時間必須小於redis中session的存活時間,就是當redus的session死亡,cahe中的session必定死亡,爲了保證這一特色,cache中的session的存活時間應該設置爲s級,設置爲1s比較合適,而且存活時間固定不能刷新,不能隨着訪問而延長存活。redis
/** * */ package com.uc56.web.omg.authentication; import java.io.Serializable; import java.util.Collection; import java.util.Date; import java.util.HashSet; import java.util.Set; import org.apache.shiro.session.ExpiredSessionException; import org.apache.shiro.session.Session; import org.apache.shiro.session.UnknownSessionException; import org.apache.shiro.session.mgt.ValidatingSession; import org.apache.shiro.session.mgt.eis.CachingSessionDAO; import org.apache.shiro.subject.support.DefaultSubjectContext; import org.crazycake.shiro.SerializeUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.uc56.web.omg.shiroredis.CustomRedisManager; /** * 將從redis讀取的session進行本地緩存,本地緩存失效時從新從redis讀取並更新最後訪問時間,解決shiro頻繁讀取redis問題 */ public class CachingShiroSessionDao extends CachingSessionDAO { private static final Logger logger = LoggerFactory.getLogger(CachingShiroSessionDao.class); /** 保存到Redis中key的前綴 */ private String keyPrefix = ""; /** * jedis 操做redis的封裝 */ private CustomRedisManager redisManager; /** * 如DefaultSessionManager在建立完session後會調用該方法; * 如保存到關係數據庫/文件系統/NoSQL數據庫;便可以實現會話的持久化; * 返回會話ID;主要此處返回的ID.equals(session.getId()); */ @Override protected Serializable doCreate(Session session) { // 建立一個Id並設置給Session Serializable sessionId = this.generateSessionId(session); assignSessionId(session, sessionId); this.saveSession(session); return sessionId; } /** * 重寫CachingSessionDAO中readSession方法,若是Session中沒有登錄信息就調用doReadSession方法從Redis中重讀 */ @Override public Session readSession(Serializable sessionId) throws UnknownSessionException { Session session = getCachedSession(sessionId); if (session == null || session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY) == null) { session = this.doReadSession(sessionId); if (session == null) { throw new UnknownSessionException("There is no session with id [" + sessionId + "]"); } else { // 緩存 cache(session, session.getId()); } } return session; } /** * 根據會話ID獲取會話 * * @param sessionId 會話ID * @return */ @Override protected Session doReadSession(Serializable sessionId) { ShiroSession shiroSession = null; try { shiroSession = (ShiroSession)SerializeUtils.deserialize(redisManager.get(this.getByteKey(sessionId))); if (shiroSession != null && shiroSession.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY) != null) { //檢查session是否過時 shiroSession.validate(); // 重置Redis中Session的最後訪問時間 shiroSession.setLastAccessTime(new Date()); this.saveSession(shiroSession); logger.info("sessionId {} name {} 被讀取並更新訪問時間", sessionId, shiroSession.getClass().getName()); } } catch (Exception e) { if (!(e instanceof ExpiredSessionException)) { logger.warn("讀取Session失敗", e); }else { logger.warn("session已失效:{}", e.getMessage()); } } return shiroSession; } //擴展更新緩存機制,每次請求不從新更新session,更新session會延長session的失效時間 @Override public void update(Session session) throws UnknownSessionException { doUpdate(session); if (session instanceof ValidatingSession) { if (((ValidatingSession) session).isValid()) { //不更新ehcach中的session,使它在設定的時間內過時 //cache(session, session.getId()); } else { uncache(session); } } else { cache(session, session.getId()); } } /** * 更新會話;如更新會話最後訪問時間/中止會話/設置超時時間/設置移除屬性等會調用 */ @Override protected void doUpdate(Session session) { //若是會話過時/中止 不必再更新了 try { if (session instanceof ValidatingSession && !((ValidatingSession) session).isValid()) { return; } } catch (Exception e) { logger.error("ValidatingSession error"); } try { if (session instanceof ShiroSession) { // 若是沒有主要字段(除lastAccessTime之外其餘字段)發生改變 ShiroSession shiroSession = (ShiroSession) session; if (!shiroSession.isChanged()) { return; } shiroSession.setChanged(false); this.saveSession(session); logger.info("sessionId {} name {} 被更新", session.getId(), session.getClass().getName()); } else if (session instanceof Serializable) { this.saveSession(session); logger.info("sessionId {} name {} 做爲非ShiroSession對象被更新, ", session.getId(), session.getClass().getName()); } else { logger.warn("sessionId {} name {} 不能被序列化 更新失敗", session.getId(), session.getClass().getName()); } } catch (Exception e) { logger.warn("更新Session失敗", e); } } /** * 刪除會話;當會話過時/會話中止(如用戶退出時)會調用 */ @Override protected void doDelete(Session session) { try { redisManager.del(this.getByteKey(session.getId())); logger.debug("Session {} 被刪除", session.getId()); } catch (Exception e) { logger.warn("修改Session失敗", e); } } /** * 刪除cache中緩存的Session */ public void uncache(Serializable sessionId) { Session session = this.readSession(sessionId); super.uncache(session); logger.info("取消session {} 的緩存", sessionId); } /** * * 統計當前活動的session */ @Override public Collection<Session> getActiveSessions() { Set<Session> sessions = new HashSet<Session>(); Set<byte[]> keys = redisManager.keys(this.keyPrefix + "*"); if(keys != null && keys.size()>0){ for(byte[] key:keys){ Session s = (Session)SerializeUtils.deserialize(redisManager.get(key)); sessions.add(s); } } return sessions; } /** * save session * @param session * @throws UnknownSessionException */ private void saveSession(Session session) throws UnknownSessionException{ if(session == null || session.getId() == null){ logger.error("session or session id is null"); return; } byte[] key = getByteKey(session.getId()); byte[] value = SerializeUtils.serialize(session); session.setTimeout(redisManager.getExpire() * 1L); this.redisManager.set(key, value, redisManager.getExpire()); } /** * 將key轉換爲byte[] * @param key * @return */ private byte[] getByteKey(Serializable sessionId){ String preKey = this.keyPrefix + sessionId; return preKey.getBytes(); } public CustomRedisManager getRedisManager() { return redisManager; } public void setRedisManager(CustomRedisManager redisManager) { this.redisManager = redisManager; /** * 初使化RedisManager */ this.redisManager.init(); } /** * 獲取 保存到Redis中key的前綴 * @return keyPrefix */ public String getKeyPrefix() { return keyPrefix; } /** * 設置 保存到Redis中key的前綴 * @param keyPrefix 保存到Redis中key的前綴 */ public void setKeyPrefix(String keyPrefix) { this.keyPrefix = keyPrefix; } }
/** * */ package com.uc56.web.omg.authentication; import java.io.Serializable; import java.util.Date; import java.util.Map; import org.apache.shiro.session.mgt.SimpleSession; /** * 因爲SimpleSession lastAccessTime更改後也會調用SessionDao update方法, * 增長標識位,若是隻是更新lastAccessTime SessionDao update方法直接返回 */ public class ShiroSession extends SimpleSession implements Serializable { /** * */ private static final long serialVersionUID = 1L; // 除lastAccessTime之外其餘字段發生改變時爲true private boolean isChanged; public ShiroSession() { super(); this.setChanged(true); } public ShiroSession(String host) { super(host); this.setChanged(true); } @Override public void setId(Serializable id) { super.setId(id); this.setChanged(true); } @Override public void setStopTimestamp(Date stopTimestamp) { super.setStopTimestamp(stopTimestamp); this.setChanged(true); } @Override public void setExpired(boolean expired) { super.setExpired(expired); this.setChanged(true); } @Override public void setTimeout(long timeout) { super.setTimeout(timeout); this.setChanged(true); } @Override public void setHost(String host) { super.setHost(host); this.setChanged(true); } @Override public void setAttributes(Map<Object, Object> attributes) { super.setAttributes(attributes); this.setChanged(true); } @Override public void setAttribute(Object key, Object value) { super.setAttribute(key, value); this.setChanged(true); } @Override public Object removeAttribute(Object key) { this.setChanged(true); return super.removeAttribute(key); } //更新最後訪問時間不更新redis @Override public void touch() { this.setChanged(false); super.touch(); } /** * 中止 */ @Override public void stop() { super.stop(); this.setChanged(true); } /** * 設置過時 */ @Override protected void expire() { this.stop(); this.setExpired(true); } public boolean isChanged() { return isChanged; } public void setChanged(boolean isChanged) { this.isChanged = isChanged; } @Override public boolean equals(Object obj) { return super.equals(obj); } @Override protected boolean onEquals(SimpleSession ss) { return super.onEquals(ss); } @Override public int hashCode() { return super.hashCode(); } @Override public String toString() { return super.toString(); } }
/** * */ package com.uc56.web.omg.authentication; import org.apache.shiro.session.Session; import org.apache.shiro.session.mgt.SessionContext; import org.apache.shiro.session.mgt.SessionFactory; public class ShiroSessionFactory implements SessionFactory { @Override public Session createSession(SessionContext initData) { ShiroSession session = new ShiroSession(); return session; } }
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="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.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa-1.0.xsd"> <!-- 自定義權限定義 --> <bean id="permissionsRealm" class="com.uc56.web.omg.realm.PermissionsRealm"> <!-- 緩存管理器 --> <property name="cacheManager" ref="shiroRedisCacheManager" /> </bean> <!-- 安全管理器 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <!-- 緩存管理器 --> <property name="cacheManager" ref="shiroEhcacheManager" /> <!-- session 管理器 --> <property name="sessionManager" ref="sessionManager" /> <property name="realm" ref="permissionsRealm"/> </bean> <!-- redis 緩存管理器 --> <bean id="shiroRedisCacheManager" class="com.uc56.web.omg.shiroredis.CustomRedisCacheManager"> <property name="redisManager" ref="shiroRedisManager" /> </bean> <bean id="shiroRedisManager" class="com.uc56.web.omg.shiroredis.CustomRedisManager"> <property name="host" value="${redis.host}" /> <property name="port" value="${redis.port}" /> <property name="password" value="${redis.password}" /> <property name="expire" value="${session.maxInactiveInterval}" /> <property name="timeout" value="${redis.timeout}" /> </bean> <!-- 提供單獨的redis Dao --> <!-- <bean id="redisSessionDAO" class="com.uc56.web.omg.shiroredis.CustomRedisSessionDAO"> <property name="redisManager" ref="shiroRedisManager" /> <property name="keyPrefix" value="${session.redis.namespace}"></property> </bean> --> <bean id="sessionDao" class="com.uc56.web.omg.authentication.CachingShiroSessionDao"> <property name="keyPrefix" value="${session.redis.namespace}"/> <property name="redisManager" ref="shiroRedisManager" /> </bean> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login/loginAuthc.do"></property> <property name="successUrl" value="login/loginIndex.do"></property> <property name="unauthorizedUrl" value="login/forbidden.do" /> <property name="filters"> <map> <entry key="authc" value-ref="formAuthenticationFilter"/> <entry key="LoginFailureCheck" value-ref="LoginFailureCheckFilter"/> </map> </property> <property name="filterChainDefinitions"> <value> /login/login.do=anon /login/loginAuthc.do=anon /login/authCheck.do=anon /login/forbidden.do=anon /login/validateUser.do=anon /city/**=anon /easyui-themes/**=anon /images/**=anon /jquery-easyui-1.5.1/**=anon /scripts/**=anon /users/**=anon /**=LoginFailureCheck,authc,user </value> </property> </bean> <!-- 用戶受權信息Cache, 採用EhCache,本地緩存最長時間應比中央緩存時間短一些,以確保Session中doReadSession方法調用時更新中央緩存過時時間 --> <bean id="shiroEhcacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager"> <property name="cacheManagerConfigFile" value="classpath:springShiro/spring-shiro-ehcache.xml"/> </bean> <bean id="formAuthenticationFilter" class="org.apache.shiro.web.filter.authc.FormAuthenticationFilter"/> <bean id="LoginFailureCheckFilter" class="com.uc56.web.omg.filter.LoginFailureCheckFilter"> <property name="casService" ref="casService"></property> <property name="loginUserService" ref="loginUserService"></property> </bean> <bean id="loginUserService" class="com.uc56.web.omg.control.LoginUserService"/> <bean id="passwordEncoder" class="com.uc56.core.security.MD5PasswordEncoder"/> <!-- session管理器 --> <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> <!-- 超時時間 --> <property name="globalSessionTimeout" value="${session.global.timeout}" /> <property name="sessionFactory" ref="sessionFactory"/> <!-- session存儲的實現 --> <property name="sessionDAO" ref="sessionDao" /> <!-- 定時檢查失效的session --> <property name="sessionValidationSchedulerEnabled" value="true" /> <!-- <property name="sessionValidationInterval" value="180000"/> --> <property name="sessionIdCookie" ref="sharesession" /> <property name="sessionListeners"> <list> <bean class="com.uc56.web.omg.authentication.listener.ShiroSessionListener"/> </list> </property> </bean> <!-- sessionIdCookie的實現,用於重寫覆蓋容器默認的JSESSIONID --> <bean id="sharesession" class="org.apache.shiro.web.servlet.SimpleCookie"> <!-- cookie的name,對應的默認是JSESSIONID --> <constructor-arg name="name" value="redisManager.sessionname" /> <!-- jsessionId的path爲/用於多個系統共享jsessionId --> <property name="path" value="/" /> <property name="httpOnly" value="false"/> </bean> <!-- 自定義Session工廠方法 返回會標識是否修改主要字段的自定義Session--> <bean id="sessionFactory" class="com.uc56.web.omg.authentication.ShiroSessionFactory"/> </beans>
<?xml version="1.0" encoding="UTF-8"?> <ehcache updateCheck="false" name="shirocache"> <!-- <diskStore path="java.io.tmpdir"/> 登陸記錄緩存 鎖定10分鐘 <cache name="passwordRetryCache" maxEntriesLocalHeap="2000" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" statistics="true"> </cache> <cache name="authorizationCache" maxEntriesLocalHeap="2000" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" statistics="true"> </cache> <cache name="authenticationCache" maxEntriesLocalHeap="2000" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" statistics="true"> </cache> <cache name="shiro-activeSessionCache" maxEntriesLocalHeap="2000" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" statistics="true"> </cache> <cache name="shiro_cache" maxElementsInMemory="2000" maxEntriesLocalHeap="2000" eternal="false" timeToIdleSeconds="0" timeToLiveSeconds="0" maxElementsOnDisk="0" overflowToDisk="true" memoryStoreEvictionPolicy="FIFO" statistics="true"> </cache> --> <!-- <defaultCache 在內存中最大的對象數量 maxElementsInMemory="10000" 設置元素是否永久的 eternal="false" 設置元素過時前的空閒時間 timeToIdleSeconds="60" 緩存數據的生存時間(TTL) timeToLiveSeconds="60" 是否當memory中的數量達到限制後,保存到Disk overflowToDisk="false" diskPersistent="false" 磁盤失效線程運行時間間隔,默認是120秒 diskExpiryThreadIntervalSeconds="10" 緩存滿了以後的淘汰算法: LRU(最近最少使用)、FIFO(先進先出)、LFU(較少使用) memoryStoreEvictionPolicy="LRU" /> --> <defaultCache maxElementsInMemory="10000" eternal="false" timeToLiveSeconds="60" overflowToDisk="false" diskPersistent="false" diskExpiryThreadIntervalSeconds="10" /> </ehcache>
此設計中最重要的一點就是:算法
1.cache中的session只存儲不更新,也就是說每次訪問不會刷新緩存中的session,cache中的session必定會在設定的時間中過時
2.cache中設置的session的時間必定要短於redis中存儲的session,保證redis中session過時是,cache中的session必定過時spring
3.redis中的session更新會清楚cache中的session保證session一直性數據庫