Client Server集群模式下session问题[转]
当我们CAS Server准备好后,就要处理Client接入的问题,如果我们的Client服务是单机模式那没有任何问题,一旦放到集群环境下就会发生如下有意思的事情。
我前面说了CAS在授权回调时会做几件事,第一TG保存到Cookie,第二个保存ticketid对应的session关系以及session对象。
那么如果我们的Client服务是集群的会发生什么?
举个例子:
我的APP服务部署了2台服务(S1、S2)采用loadbalance映射一个域名出去访问,当CAS授权回调时被loadbalance路由到S1上,SingleSignOutFilter以及SingleSignOutHandler进行了TGC和SessionMappingStorage,默认的持久化方式是hash的方式,也就是说本地map方式,这样在下次访问到APP时被loadbalance路由到S2上就会发生什么有意思的事情呢?我相信做过分布式服务的应该都能猜出来什么问题。
APP:我没找到cas认证信息,跳转到cas login页面
CAS:我找到了你APP已经做过认证了,跳转到APP并且给你上次认证的ticlet
APP:我真没找到你的认证信息,跳转到cas login页面
CAS:你真的已经做过认证了,跳转到APP并且给你上次认证的ticlet
这样就会发生无线跳转死循环问题。
那如何解决上面的问题呢?
在分布式的环境下几乎服务都是集群的,甚至有很多公司会做异地多活等等。那么在集群环境下如何解决cas授权持久化的问题呢?很简单重新实现一个cas-client的SessionMappingStorage,这里可以使用很多方式,比如说:放到db、nosql的存储上(mongodb、redis)、memcache、分布式文件存储都可以。
我这里采用的是redis,而且我们dev和qa环境采用单机模式,stage和prod环境使用集群模式,因此我还做了集群和本地都兼容的方式,话不多说直接贴出实现代码
import java.util.HashMap; import java.util.Map; import javax.servlet.http.HttpSession; import org.jasig.cas.client.session.SessionMappingStorage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import xxxxxxxx.framework.redis.client.IRedisClient; public class RedisBackedSessionMappingStorage implements SessionMappingStorage { private final Logger logger = LoggerFactory.getLogger(getClass()); /** * Maps the ID from the CAS server to the Session. */ private final Map<String, HttpSession> MANAGED_SESSIONS = new HashMap<String, HttpSession>(); /** * Maps the Session ID to the key from the CAS Server. */ private final Map<String, String> ID_TO_SESSION_KEY_MAPPING = new HashMap<String, String>(); private final static String NAME_SPACE = "CAS"; private IRedisClient redisClient; /** * 在dev和qa环境使用单机模式:hash * 在stage和prod环境使用集群模式:redis */ private String storageMode = "hash"; /** * 获取 redisClient * @return the redisClient */ public IRedisClient getRedisClient() { return redisClient; } /** * 设置 redisClient * @param redisClient the redisClient to set */ public void setRedisClient(IRedisClient redisClient) { this.redisClient = redisClient; } /** * 获取 storageMode * @return the storageMode */ public String getStorageMode() { return storageMode; } /** * 设置 storageMode * @param storageMode the storageMode to set */ public void setStorageMode(String storageMode) { this.storageMode = storageMode; } @Override public HttpSession removeSessionByMappingId(String mappingId) { HttpSession session = null; if (storageMode.equals("hash")) { session = MANAGED_SESSIONS.get(mappingId); } else { session = redisClient.get(mappingId, NAME_SPACE, HttpSession.class, null); } if (session != null) { removeBySessionById(session.getId()); } return session; } @Override public void removeBySessionById(String sessionId) { logger.debug("Attempting to remove Session=[{}]", sessionId); String key = null; if (storageMode.equals("hash")) { key = ID_TO_SESSION_KEY_MAPPING.get(sessionId); } else { key = redisClient.get(sessionId, NAME_SPACE, null); } if (logger.isDebugEnabled()) { if (key != null) { logger.debug("Found mapping for session. Session Removed."); } else { logger.debug("No mapping for session found. Ignoring."); } } if (storageMode.equals("hash")) { MANAGED_SESSIONS.remove(key); ID_TO_SESSION_KEY_MAPPING.remove(sessionId); } else { redisClient.del(key, NAME_SPACE); redisClient.del(sessionId, NAME_SPACE); } } @Override public void addSessionById(String mappingId, HttpSession session) { if (storageMode.equals("hash")) { ID_TO_SESSION_KEY_MAPPING.put(session.getId(), mappingId); MANAGED_SESSIONS.put(mappingId, session); } else { redisClient.set(session.getId(), NAME_SPACE, mappingId, -1); redisClient.set(mappingId, NAME_SPACE, session, -1); } } }
这里使用的redis-client是我自己封装,使用文档在:《RedisClient使用说明》,支持redis集群模式:《RedisClient升级支持Sentinel使用说明》,代码已经放到了github上:
项目地址:redis-client
把上面的RedisBackedSessionMappingStorage
类注入到org.jasig.cas.client.session.SingleSignOutFilter
中即可
<bean id="singleLogoutFilter" class="org.jasig.cas.client.session.SingleSignOutFilter"> <property name="sessionMappingStorage" ref="redisBackedSessionMappingStorage"></property> </bean> <bean id="redisBackedSessionMappingStorage" class="xxxxxxx.cas.session.storage.RedisBackedSessionMappingStorage"> <property name="redisClient" ref="redisClient"></property> <property name="storageMode" value="${cas.session.storage.mode}"></property> </bean>
ps.参数cas.session.storage.mode,值:hash(本地map)、redis(集中存储)
- 本文作者: 凝雨-Yun
- 本文标题: CAS使用经验总结,纯干货
- 本文链接: https://ningyu1.github.io/site/post/54-cas-server/
- 发布时间:2018-01-19
- 版权声明: 本文由 凝雨-Yun 原创,采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。
转载请保留以上声明信息!