Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing Redis-Based Global Session Mapping for CAS Single Sign-Out in Clustered Environments

Tech May 17 2

In a distributed environment where multiple CAS client instances sit behind a load balancer like Nginx, implementing Single Sign-Out (SLO) presents a significant challenge. By default, the CAS client uses an in-memory HashMap to store the relationship between Service Tickets (ST) and Session IDs. When the CAS server sends a logout request to a cliant node, the load balancer may route that request to an instance that did not originally create the session. Consequently, the local map lookup fails, and the user remains logged in on other nodes.

To resolve this, the session mapping must be externalized to a shared store like Redis. This ensures that any node in the cluster can identify and invalidate the correct session upon receiving a logout signal.

Dependency Configuration

To implement this solution, ensure your project includes the following dependencies (or compatible versions):

  • spring-data-redis-1.7.4.RELEASE
  • jedis-2.9.0
  • fastjson-1.2.31

Custom Redis Session Mapping Storage

The following implementation replaces the default HashMapBackedSessionMappingStorage. It persists the bidirectional mapping between CAS tickets and Web sessions into a Redis cluster.

package com.distributed.cas.client.session;

import org.jasig.cas.client.session.SessionMappingStorage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;

import javax.servlet.http.HttpSession;
import java.util.concurrent.TimeUnit;

public final class RedisSessionMappingStore implements SessionMappingStorage {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    private static final String KEY_SESSION_TO_TICKET = "CAS_STORE:SESS_TO_TKT:";
    private static final String KEY_TICKET_TO_SESSION = "CAS_STORE:TKT_TO_SESS:";

    private int expirySeconds = 86400; // 24 hours
    private RedisTemplate<String, String> redisTemplate;

    public RedisSessionMappingStore() {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring/redis-config.xml");
        this.redisTemplate = (RedisTemplate<String, String>) context.getBean("redisTemplate");
    }

    @Override
    public synchronized void addSessionById(String mappingId, HttpSession session) {
        String sessionId = session.getId();
        logger.debug("Mapping ticket {} to session {}", mappingId, sessionId);
        try {
            redisTemplate.boundValueOps(KEY_SESSION_TO_TICKET + sessionId).set(mappingId, expirySeconds, TimeUnit.SECONDS);
            redisTemplate.boundValueOps(KEY_TICKET_TO_SESSION + mappingId).set(sessionId, expirySeconds, TimeUnit.SECONDS);
        } catch (Exception e) {
            logger.error("Error storing session mapping in Redis", e);
        }
    }

    @Override
    public synchronized void removeBySessionById(String sessionId) {
        logger.debug("Invalidating session via ID: {}", sessionId);
        String ticketKey = KEY_SESSION_TO_TICKET + sessionId;
        String ticket = redisTemplate.opsForValue().get(ticketKey);

        if (ticket != null) {
            redisTemplate.delete(KEY_TICKET_TO_SESSION + ticket);
            logger.debug("Removed ticket mapping for {}", ticket);
        }
        redisTemplate.delete(ticketKey);
    }

    @Override
    public synchronized HttpSession removeSessionByMappingId(String mappingId) {
        String sessionKey = KEY_TICKET_TO_SESSION + mappingId;
        String sessionId = redisTemplate.opsForValue().get(sessionKey);

        if (sessionId != null) {
            removeBySessionById(sessionId);
            // Note: Since HttpSession objects cannot be serialized easily across JVMs 
            // in standard CAS client filters, we trigger the manual removal logic here.
        }
        return null;
    }
}

Redis Template Configuration

The RedisTemplate must be configured to handle string-based keys for the session mappings. Below is an example XML configuraton for a Redis Cluster environment.

<?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:context="http://www.springframework.org/schema/context"
       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">

    <context:property-placeholder location="classpath:redis-cluster.properties" />

    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <property name="maxIdle" value="${redis.maxIdle}" />
        <property name="maxWaitMillis" value="${redis.maxWait}" />
        <property name="testOnBorrow" value="true" />
    </bean>

    <bean id="redisClusterConfig" class="org.springframework.data.redis.connection.RedisClusterConfiguration">
        <constructor-arg name="propertySource">
            <bean class="org.springframework.core.io.support.ResourcePropertySource">
                <constructor-arg value="classpath:redis-cluster.properties" />
            </bean>
        </constructor-arg>
    </bean>

    <bean id="jeidConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <constructor-arg ref="redisClusterConfig" />
        <constructor-arg ref="jedisPoolConfig" />
        <property name="password" value="${redis.password}" />
        <property name="timeout" value="${redis.timeout}" />
    </bean>

    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory" ref="jeidConnectionFactory" />
        <property name="keySerializer">
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
        </property>
        <property name="valueSerializer">
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
        </property>
    </bean>
</beans>

Cluster Properties Definition

Configure your Redis nodes and connection pool settings in the redis-cluster.properties file. Ensure the cluster node list is complete for optimal redirection handling.

# Redis Pool Settings
redis.password=your_secure_password
redis.maxIdle=50
redis.maxWait=2000
redis.timeout=5000

# Redis Cluster Nodes
spring.redis.cluster.nodes=10.0.0.1:6379,10.0.0.2:6379,10.0.0.3:6379,10.0.0.1:6380,10.0.0.2:6380,10.0.0.3:6380
spring.redis.cluster.max-redirects=3

By integrating this RedisSessionMappingStore into your SingleSignOutFilter configuration, the logout requests from the CAS server will successfully locate and remove session identifiers regardless of which cluster node receives the callback.

Tags: CAS

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.