目前安全框架shiro使用较为广泛,其功能也比较强大。为了分布式session共享,通常的做法是将session存储在redis中,实现多个节点获取同一个session。此实现可以实现session共享,但session的特点是内存存储,就是为了高速频繁访问,每个请求都必须验证session是否存在是否过期,也从session中获取数据。这样导致一个页面刷新过程中的数十个请求会同时访问redis,在几毫秒内同时操作session的获取,修改,更新,保存,删除等操作,从而造成redis的并发量飙升,刷新一个页面操作redis几十到几百次。
为了解决由于session共享造成的redis高并发问题,很明显需要在redis之前做一次短暂的session缓存,如果该缓存存在就不用从redis中获取,从而减少同时访问redis的次数。如果做session缓存,主要有两种种方案,其实原理都相同:
1>重写sessionManager的retrieveSession方法。首先从request中获取session,如果request中不存在再走原来的从redis中获取。这样可以让一个请求的多次访问redis问题得到解决,因为request的生命周期为浏览器发送一个请求到接收服务器的一次响应完成,因此,在一次请求中,request中的session是一直存在的,并且不用担心session超时过期等的问题。这样就可以达到有多少次请求就几乎有多少次访问redis,大大减少单次请求,频繁访问redis的问题。大大减少redis的并发数量。此实现方法最为简单。
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比较合适,并且存活时间固定不能刷新,不能随着访问而延长存活。
/** * */ 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一定过期
3.redis中的session更新会清楚cache中的session保证session一直性