CAS Client集群环境的Session问题及解决方案 不能退出登录(Session problem and solution in CAS client cluster environment: unable to log out)

casclient源代码下载链接:https://github.com/apereo/java-cas-client

cas官网链接:https://www.apereo.org/projects/cas

1.上面一篇引用别人的分析方案介绍,来描述了下项目中遇到的问题,现在介绍本人怎么解决的

2.本人项目中用的是改造了tomcat 做的session 共享

3.所以客户端请求退出,服务端根据TGT查看对应的ST进行请求客户端,通过nginx负载均衡,可能对应到另一台客户端服务器,但是我们的session是存入rediscluster,任意客户端可以根据sessionid取到这个,进行删除,这样session就没了。即在客户端配置的SingleSignOutFilter,需要在HashMapBackedSessionMappingStorage进行删除存入redis的session数据,这样就可以退出了,就是这个原理来处理这个退出登录问题

一.上代码依赖jar包

spring-data-redis-1.7.4.RELEASE.jar;jedis-2.9.0.jar;fastjson-1.2.31.jar;注意jar包版本

二.改造的HashMapBackedSessionMappingStorage类代码

package org.jasig.cas.client.session;
 
/*
 * agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership.
 * Jasig licenses this file to you under the Apache License,
 * Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License.  You may obtain a
 * copy of the License at the following location:
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
 
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
 
import javax.annotation.Resource;
import javax.servlet.http.HttpSession;
 
import org.jasig.cas.client.session.SessionMappingStorage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.EnableMBeanExport;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
 
/**
 * HashMap backed implementation of SessionMappingStorage.
 *
 * @author Scott Battaglia
 * @version $Revision$ $Date$
 * @since 3.1
 *
 */
public final class HashMapBackedSessionMappingStorage implements SessionMappingStorage {
 
      protected final transient Logger logger = LoggerFactory.getLogger(getClass());
 
     private final static String CASCLIENT_PREFIX = "CASCLI:SESSIONID:";
     private final static String CASCLIENT_MAPID_PREFIX = "CASCLI:MAPID:";
 
     private int casTimeout=86400;
 
     private RedisTemplate redisTemplate=new RedisTemplate();
 
    public RedisTemplate getRedisTemplate() {
        return redisTemplate;
    }
 
    public void setRedisTemplate(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
 
    public  HashMapBackedSessionMappingStorage(){
        ApplicationContext ac =new ClassPathXmlApplicationContext("classpath:schemeone/xml/spring-core.xml");
        setRedisTemplate((RedisTemplate)ac.getBean("redisTemplate"));
     }
 
    @Override
    public synchronized void addSessionById(String mappingId, HttpSession session) {
            logger.debug("Adding ticket {}", session);
            try {
                String sessionRedisKey = this.getCasSessionRedisKey(session.getId());
                String mappingIdRedisKey = this.getCasMappingIdRedisKey(mappingId);
                this.redisTemplate.boundValueOps(sessionRedisKey).set(mappingId, casTimeout, TimeUnit.SECONDS);
                this.redisTemplate.boundValueOps(mappingIdRedisKey).set(session.getId(), casTimeout, TimeUnit.SECONDS);
            } catch (final Exception e) {
                logger.error("Failed Adding {}", session, e);
            }
    }
 
    @Override
    public synchronized void removeBySessionById(String sessionId) {
         logger.debug("Attempting to remove Session=[{}]", sessionId);
 
            final String key =(String) this.redisTemplate.boundValueOps(this.getCasSessionRedisKey(sessionId)).get(); 
 
            if (logger.isDebugEnabled()) {
                if (key != null) {
                    logger.debug("Found mapping for session.  Session Removed.");
                } else {
                    logger.debug("No mapping for session found.  Ignoring.");
                }
            }
            this.redisTemplate.delete(this.getCasMappingIdRedisKey(key));
            this.redisTemplate.delete(this.getCasSessionRedisKey(sessionId));
    }
 
    @Override
    public synchronized HttpSession removeSessionByMappingId(String mappingId) {
        //先去取sessionid
        final String sessionId=(String) this.redisTemplate.boundValueOps(this.getCasMappingIdRedisKey(mappingId)).get();
        //final HttpSession session = (HttpSession) this.redisTemplate.boundValueOps(sessionId).get();
         this.redisTemplate.delete(sessionId);
//            if (session != null) {
//                removeBySessionById(session.getId());
//            }
//            return session;
         if (sessionId != null) {
                removeBySessionById(sessionId);
            }
            return null;
    }
 
      private String getCasSessionRedisKey( String sessionId) {
            return CASCLIENT_PREFIX + sessionId;
        }
 
      private String getCasMappingIdRedisKey(String mappingId) {
            return CASCLIENT_MAPID_PREFIX + mappingId;
        }
 
}

三。redistemplteBean配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:util="http://www.springframework.org/schema/util"
	xsi:schemaLocation="
          http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
          http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
          http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
          http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
 
	<!-- 扫描注解Bean -->
	<context:component-scan base-package="com.hivescm" />
 
	<aop:config proxy-target-class="true" />
 
	<!-- 开启AOP监听 只对当前配置文件有效 -->
	<aop:aspectj-autoproxy expose-proxy="true" />
 
		<bean id="propertyConfigurer"
		class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
		<property name="locations">
			<list>
<!-- 				<value>classpath:schemeone/properties/common/*.properties</value> -->
					<value>classpath:schemeone/properties/common/redis.cluster.properties</value>
			</list>
		</property>
	</bean>
		<!--     jedis 配置 -->
    <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig" >
<!--         最大空闲数 -->
        <property name="maxIdle" value="${redis.maxIdle}" />
<!--         最大建立连接等待时间 -->
        <property name="maxWaitMillis" value="${redis.maxWait}" />
<!--         是否在从池中取出连接前进行检验,如果检验失败,则从池中去除连接并尝试取出另一个 -->
        <property name="testOnBorrow" value="${redis.testOnBorrow}" />
    </bean>
 
<!--     配置文件加载 -->
    <bean id="resourcePropertySource" class="org.springframework.core.io.support.ResourcePropertySource">
        <constructor-arg name="name" value="redis.cluster.properties"/>
        <constructor-arg name="resource" value="classpath:schemeone/properties/common/redis.cluster.properties"/>
    </bean>
 
<!--     redisCluster配置 -->
    <bean id="redisClusterConfiguration" class="org.springframework.data.redis.connection.RedisClusterConfiguration">
        <constructor-arg name="propertySource" ref="resourcePropertySource"/>
    </bean>
 
<!--     redis服务器中心 -->
    <bean id="connectionFactory"  class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" >
        <constructor-arg name="clusterConfig" ref="redisClusterConfiguration"/>
        <constructor-arg name="poolConfig" ref="poolConfig"/>
        <property name="password" value="${redis.password}" />
        <property name="timeout" value="${redis.timeout}" ></property>
    </bean >
 
    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate" >
        <property name="connectionFactory" ref="connectionFactory" />
<!--         如果不配置Serializer,那么存储的时候缺省使用String,如果用User类型存储,那么会提示错误User can't cast to String!!  -->
        <property name="keySerializer" >
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
        </property>
        <property name="valueSerializer" >
            <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer" />
        </property>
        <property name="hashKeySerializer">
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
        </property>
        <property name="hashValueSerializer">
            <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>
        </property>
    </bean >
 
</beans>

  4.redis.cluster.properties

#redis\u4E2D\u5FC3
#redis\u7684\u670D\u52A1\u5668\u5730\u5740
redis.host=192.168.103.158
#redis\u7684\u670D\u52A1\u7AEF\u53E3
redis.port=6379
#\u5BC6\u7801
redis.password=
#\u6700\u5927\u7A7A\u95F2\u6570
redis.maxIdle=100
#\u6700\u5927\u8FDE\u63A5\u6570
redis.maxActive=300
#\u6700\u5927\u5EFA\u7ACB\u8FDE\u63A5\u7B49\u5F85\u65F6\u95F4
redis.maxWait=1000
#\u5BA2\u6237\u7AEF\u8D85\u65F6\u65F6\u95F4\u5355\u4F4D\u662F\u6BEB\u79D2
redis.timeout=100000
redis.maxTotal=1000
redis.minIdle=8
#\u660E\u662F\u5426\u5728\u4ECE\u6C60\u4E2D\u53D6\u51FA\u8FDE\u63A5\u524D\u8FDB\u884C\u68C0\u9A8C,\u5982\u679C\u68C0\u9A8C\u5931\u8D25,\u5219\u4ECE\u6C60\u4E2D\u53BB\u9664\u8FDE\u63A5\u5E76\u5C1D\u8BD5\u53D6\u51FA\u53E6\u4E00\u4E2A
redis.testOnBorrow=true
 
#sentinel
#spring.redis.sentinel.node1.host=127.0.0.1
#spring.redis.sentinel.node2.host=127.0.0.1
#spring.redis.sentinel.node3.host=127.0.0.1
#spring.redis.sentinel.node1.port=26379
#spring.redis.sentinel.node2.port=26479
#spring.redis.sentinel.node3.port=26579
#sentinel
 
#jediscluster
#cluster1.host.port=127.0.0.1:7000
#cluster2.host.port=127.0.0.1:7001
#cluster3.host.port=127.0.0.1:7002
#cluster4.host.port=127.0.0.1:7003
#cluster5.host.port=127.0.0.1:7004
#cluster6.host.port=127.0.0.1:7005
#cluster7.host.port=127.0.0.1:7006
#cluster8.host.port=127.0.0.1:7007
#jediscluster
 
#rediscluster
#spring.redis.cluster.nodes=192.168.103.158:6379
spring.redis.cluster.nodes=192.168.103.174:6379,192.168.103.174:6389,192.168.103.174:6399,192.168.103.173:6379,192.168.103.173:6389,192.168.103.173:6399
spring.redis.cluster.max-redirects=3
————————

casclient源代码下载链接:https://github.com/apereo/java-cas-client

cas官网链接:https://www.apereo.org/projects/cas

1. The above article quoted others’ analysis scheme to describe the problems encountered in the next project. Now I will introduce how to solve them

2. In my project, I transformed the session sharing made by Tomcat

3. Therefore, the client requests to exit. The server checks the corresponding st according to the TGT and requests the client. Through nginx load balancing, it may correspond to another client server, but our session is stored in rediscluster. Any client can get this one according to the sessionid and delete it, so that the session is gone. That is, the singlesignoutfilter configured on the client needs to delete the session data stored in redis in hashmapbackedsessionmappingstorage, so that you can exit. This is the principle to deal with the exit login problem

I Code dependent jar package on

spring-data-redis-1.7.4.RELEASE.jar;jedis-2.9.0.jar;fastjson-1.2.31.jar;注意jar包版本

二.改造的HashMapBackedSessionMappingStorage类代码

package org.jasig.cas.client.session;
 
/*
 * agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership.
 * Jasig licenses this file to you under the Apache License,
 * Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License.  You may obtain a
 * copy of the License at the following location:
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
 
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
 
import javax.annotation.Resource;
import javax.servlet.http.HttpSession;
 
import org.jasig.cas.client.session.SessionMappingStorage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.EnableMBeanExport;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
 
/**
 * HashMap backed implementation of SessionMappingStorage.
 *
 * @author Scott Battaglia
 * @version $Revision$ $Date$
 * @since 3.1
 *
 */
public final class HashMapBackedSessionMappingStorage implements SessionMappingStorage {
 
      protected final transient Logger logger = LoggerFactory.getLogger(getClass());
 
     private final static String CASCLIENT_PREFIX = "CASCLI:SESSIONID:";
     private final static String CASCLIENT_MAPID_PREFIX = "CASCLI:MAPID:";
 
     private int casTimeout=86400;
 
     private RedisTemplate redisTemplate=new RedisTemplate();
 
    public RedisTemplate getRedisTemplate() {
        return redisTemplate;
    }
 
    public void setRedisTemplate(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
 
    public  HashMapBackedSessionMappingStorage(){
        ApplicationContext ac =new ClassPathXmlApplicationContext("classpath:schemeone/xml/spring-core.xml");
        setRedisTemplate((RedisTemplate)ac.getBean("redisTemplate"));
     }
 
    @Override
    public synchronized void addSessionById(String mappingId, HttpSession session) {
            logger.debug("Adding ticket {}", session);
            try {
                String sessionRedisKey = this.getCasSessionRedisKey(session.getId());
                String mappingIdRedisKey = this.getCasMappingIdRedisKey(mappingId);
                this.redisTemplate.boundValueOps(sessionRedisKey).set(mappingId, casTimeout, TimeUnit.SECONDS);
                this.redisTemplate.boundValueOps(mappingIdRedisKey).set(session.getId(), casTimeout, TimeUnit.SECONDS);
            } catch (final Exception e) {
                logger.error("Failed Adding {}", session, e);
            }
    }
 
    @Override
    public synchronized void removeBySessionById(String sessionId) {
         logger.debug("Attempting to remove Session=[{}]", sessionId);
 
            final String key =(String) this.redisTemplate.boundValueOps(this.getCasSessionRedisKey(sessionId)).get(); 
 
            if (logger.isDebugEnabled()) {
                if (key != null) {
                    logger.debug("Found mapping for session.  Session Removed.");
                } else {
                    logger.debug("No mapping for session found.  Ignoring.");
                }
            }
            this.redisTemplate.delete(this.getCasMappingIdRedisKey(key));
            this.redisTemplate.delete(this.getCasSessionRedisKey(sessionId));
    }
 
    @Override
    public synchronized HttpSession removeSessionByMappingId(String mappingId) {
        //先去取sessionid
        final String sessionId=(String) this.redisTemplate.boundValueOps(this.getCasMappingIdRedisKey(mappingId)).get();
        //final HttpSession session = (HttpSession) this.redisTemplate.boundValueOps(sessionId).get();
         this.redisTemplate.delete(sessionId);
//            if (session != null) {
//                removeBySessionById(session.getId());
//            }
//            return session;
         if (sessionId != null) {
                removeBySessionById(sessionId);
            }
            return null;
    }
 
      private String getCasSessionRedisKey( String sessionId) {
            return CASCLIENT_PREFIX + sessionId;
        }
 
      private String getCasMappingIdRedisKey(String mappingId) {
            return CASCLIENT_MAPID_PREFIX + mappingId;
        }
 
}

三。redistemplteBean配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:util="http://www.springframework.org/schema/util"
	xsi:schemaLocation="
          http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
          http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
          http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
          http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
 
	<!-- 扫描注解Bean -->
	<context:component-scan base-package="com.hivescm" />
 
	<aop:config proxy-target-class="true" />
 
	<!-- 开启AOP监听 只对当前配置文件有效 -->
	<aop:aspectj-autoproxy expose-proxy="true" />
 
		<bean id="propertyConfigurer"
		class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
		<property name="locations">
			<list>
<!-- 				<value>classpath:schemeone/properties/common/*.properties</value> -->
					<value>classpath:schemeone/properties/common/redis.cluster.properties</value>
			</list>
		</property>
	</bean>
		<!--     jedis 配置 -->
    <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig" >
<!--         最大空闲数 -->
        <property name="maxIdle" value="${redis.maxIdle}" />
<!--         最大建立连接等待时间 -->
        <property name="maxWaitMillis" value="${redis.maxWait}" />
<!--         是否在从池中取出连接前进行检验,如果检验失败,则从池中去除连接并尝试取出另一个 -->
        <property name="testOnBorrow" value="${redis.testOnBorrow}" />
    </bean>
 
<!--     配置文件加载 -->
    <bean id="resourcePropertySource" class="org.springframework.core.io.support.ResourcePropertySource">
        <constructor-arg name="name" value="redis.cluster.properties"/>
        <constructor-arg name="resource" value="classpath:schemeone/properties/common/redis.cluster.properties"/>
    </bean>
 
<!--     redisCluster配置 -->
    <bean id="redisClusterConfiguration" class="org.springframework.data.redis.connection.RedisClusterConfiguration">
        <constructor-arg name="propertySource" ref="resourcePropertySource"/>
    </bean>
 
<!--     redis服务器中心 -->
    <bean id="connectionFactory"  class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" >
        <constructor-arg name="clusterConfig" ref="redisClusterConfiguration"/>
        <constructor-arg name="poolConfig" ref="poolConfig"/>
        <property name="password" value="${redis.password}" />
        <property name="timeout" value="${redis.timeout}" ></property>
    </bean >
 
    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate" >
        <property name="connectionFactory" ref="connectionFactory" />
<!--         如果不配置Serializer,那么存储的时候缺省使用String,如果用User类型存储,那么会提示错误User can't cast to String!!  -->
        <property name="keySerializer" >
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
        </property>
        <property name="valueSerializer" >
            <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer" />
        </property>
        <property name="hashKeySerializer">
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
        </property>
        <property name="hashValueSerializer">
            <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>
        </property>
    </bean >
 
</beans>

  4.redis.cluster.properties

#redis\u4E2D\u5FC3
#redis\u7684\u670D\u52A1\u5668\u5730\u5740
redis.host=192.168.103.158
#redis\u7684\u670D\u52A1\u7AEF\u53E3
redis.port=6379
#\u5BC6\u7801
redis.password=
#\u6700\u5927\u7A7A\u95F2\u6570
redis.maxIdle=100
#\u6700\u5927\u8FDE\u63A5\u6570
redis.maxActive=300
#\u6700\u5927\u5EFA\u7ACB\u8FDE\u63A5\u7B49\u5F85\u65F6\u95F4
redis.maxWait=1000
#\u5BA2\u6237\u7AEF\u8D85\u65F6\u65F6\u95F4\u5355\u4F4D\u662F\u6BEB\u79D2
redis.timeout=100000
redis.maxTotal=1000
redis.minIdle=8
#\u660E\u662F\u5426\u5728\u4ECE\u6C60\u4E2D\u53D6\u51FA\u8FDE\u63A5\u524D\u8FDB\u884C\u68C0\u9A8C,\u5982\u679C\u68C0\u9A8C\u5931\u8D25,\u5219\u4ECE\u6C60\u4E2D\u53BB\u9664\u8FDE\u63A5\u5E76\u5C1D\u8BD5\u53D6\u51FA\u53E6\u4E00\u4E2A
redis.testOnBorrow=true
 
#sentinel
#spring.redis.sentinel.node1.host=127.0.0.1
#spring.redis.sentinel.node2.host=127.0.0.1
#spring.redis.sentinel.node3.host=127.0.0.1
#spring.redis.sentinel.node1.port=26379
#spring.redis.sentinel.node2.port=26479
#spring.redis.sentinel.node3.port=26579
#sentinel
 
#jediscluster
#cluster1.host.port=127.0.0.1:7000
#cluster2.host.port=127.0.0.1:7001
#cluster3.host.port=127.0.0.1:7002
#cluster4.host.port=127.0.0.1:7003
#cluster5.host.port=127.0.0.1:7004
#cluster6.host.port=127.0.0.1:7005
#cluster7.host.port=127.0.0.1:7006
#cluster8.host.port=127.0.0.1:7007
#jediscluster
 
#rediscluster
#spring.redis.cluster.nodes=192.168.103.158:6379
spring.redis.cluster.nodes=192.168.103.174:6379,192.168.103.174:6389,192.168.103.174:6399,192.168.103.173:6379,192.168.103.173:6389,192.168.103.173:6399
spring.redis.cluster.max-redirects=3