目前安全框架 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 的并发数量.此实现方法最为简单.
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 cn.uce.web.login.filter;
import java.io.Serializable;
import javax.servlet.ServletRequest;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.SessionKey;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.session.mgt.WebSessionKey;
public class ShiroSessionManager extends DefaultWebSessionManager {
/**
* 获取session
* 优化单次请求需要多次访问redis的问题
* @param sessionKey
* @return
* @throws UnknownSessionException
*/
@Override protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
Serializable sessionId = getSessionId(sessionKey);
ServletRequest request = null;
if (sessionKey instanceof WebSessionKey) {
request = ((WebSessionKey) sessionKey).getServletRequest();
}
if (request != null && null != sessionId) {
Object sessionObj = request.getAttribute(sessionId.toString());
if (sessionObj != null) {
return (Session) sessionObj;
}
}
Session session = super.retrieveSession(sessionKey);
if (request != null && null != sessionId) {
request.setAttribute(sessionId.toString(), session);
}
return session;
}
}
<!-- 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>
此设计中最重要的一点就是:
/**
*
*/
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 一直性
来源: http://www.bubuko.com/infodetail-2462551.html