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(集中存储)

 

 

 

posted @ 2022-05-13 16:44  zhh  阅读(112)  评论(0编辑  收藏  举报