Implementing Cross-System Single Sign-On with CAS: A Practical Guide

This article explains how to implement Single Sign-On (SSO) using CAS to unify authentication across multiple company systems, covering session mechanics, cluster session sharing with Redis, and providing complete Spring Boot code examples for both the CAS server and client applications.

Architect's Guide
Architect's Guide
Architect's Guide
Implementing Cross-System Single Sign-On with CAS: A Practical Guide

Background

When a company operates many systems, users must log in separately to each one, creating poor experience and increased password management overhead. Independent authentication systems also reduce security posture. Single Sign-On (SSO) solves this by letting users log in once and access all systems.

Traditional Session Mechanism and Authentication

Cookie and Server Interaction

HTTP is stateless; each request spawns a new server thread with no retained context. To identify a user across requests (e.g., for a shopping cart), the server creates a session identified by a JSESSIONID stored in a browser cookie (or via URL rewriting if cookies are disabled). The session lives in server memory as a hash table keyed by session ID.

Server-Side Session Mechanism

On each request, the server performs the following steps:

Reads the JSESSIONID from the cookie.

Looks up the session data in its local store.

If missing, creates a new session, generates a new JSESSIONID, and returns it in the response header.

Session-Based Authentication Flow

User submits credentials → server validates → creates session → returns JSESSIONID cookie → subsequent requests include cookie → server retrieves session → grants access.

Cluster Session Challenges and Solutions

In a load-balanced cluster, a user's requests may hit different servers. Since sessions are stored locally, a session created on Server A is invisible to Server B, breaking authentication.

Session Sharing Approaches

Session Replication : Copy session data to all nodes on login, modification, or logout. High implementation cost, maintenance complexity, and replication latency.

Centralized Session Storage : Store sessions in a shared store (typically Redis). All servers read/write from the same Redis instance, eliminating synchronization issues. This is the recommended approach.

Multi-Service Login Challenges and SSO with CAS

SSO Background

Large enterprises run many business support systems, each with its own authentication. Users must log in repeatedly. SSO enables one login for all systems using a ticket passed between systems.

CAS Principle

Because cookies cannot cross different domains, a dedicated authentication domain (e.g., oauth.com) issues a ticket after login. The flow:

User accesses b.com (unauthenticated) → redirects to oauth.com.

User logs in at oauth.com → cookie set on oauth.com domain. oauth.com stores <ticket, sessionId> in Redis, redirects back to b.com?ticket=.... b.com receives ticket, queries Redis for sessionId, creates local session, sets its own cookie, redirects to original page.

Subsequent requests to b.com include its cookie → authenticated.

CAS Login Service Demo (Spring Boot)

User Entity

public class UserForm implements Serializable {
    private static final long serialVersionUID = 1L;
    private String username;
    private String password;
    private String backurl;
    // getters and setters
}

Login Controller

@Controller
public class IndexController {
    @Autowired
    private RedisTemplate redisTemplate;

    @GetMapping("/toLogin")
    public String toLogin(Model model, HttpServletRequest request) {
        Object userInfo = request.getSession().getAttribute(LoginFilter.USER_INFO);
        if (userInfo != null) {
            String ticket = UUID.randomUUID().toString();
            redisTemplate.opsForValue().set(ticket, userInfo, 2, TimeUnit.SECONDS);
            return "redirect:" + request.getParameter("url") + "?ticket=" + ticket;
        }
        UserForm user = new UserForm();
        user.setUsername("laowang");
        user.setPassword("laowang");
        user.setBackurl(request.getParameter("url"));
        model.addAttribute("user", user);
        return "login";
    }

    @PostMapping("/login")
    public void login(@ModelAttribute UserForm user, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        request.getSession().setAttribute(LoginFilter.USER_INFO, user);
        String ticket = UUID.randomUUID().toString();
        redisTemplate.opsForValue().set(ticket, user, 20, TimeUnit.SECONDS);
        if (user.getBackurl() == null || user.getBackurl().isEmpty()) {
            response.sendRedirect("/index");
        } else {
            response.sendRedirect(user.getBackurl() + "?ticket=" + ticket);
        }
    }

    @GetMapping("/index")
    public ModelAndView index(HttpServletRequest request) {
        ModelAndView mv = new ModelAndView();
        Object user = request.getSession().getAttribute(LoginFilter.USER_INFO);
        mv.setViewName("index");
        mv.addObject("user", user);
        request.getSession().setAttribute("test", "123");
        return mv;
    }
}

Login Filter – protects non-login URLs, forwards unauthenticated requests to /toLogin.

public class LoginFilter implements Filter {
    public static final String USER_INFO = "user";
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        Object userInfo = request.getSession().getAttribute(USER_INFO);
        String requestUrl = request.getServletPath();
        if (!"/toLogin".equals(requestUrl) && !requestUrl.startsWith("/login") && userInfo == null) {
            request.getRequestDispatcher("/toLogin").forward(request, response);
            return;
        }
        chain.doFilter(request, response);
    }
    // init, destroy omitted
}

Filter Registration

@Configuration
public class LoginConfig {
    @Bean
    public FilterRegistrationBean sessionFilterRegistration() {
        FilterRegistrationBean reg = new FilterRegistrationBean();
        reg.setFilter(new LoginFilter());
        reg.addUrlPatterns("/*");
        reg.setName("sessionFilter");
        reg.setOrder(1);
        return reg;
    }
}

Login Page (Thymeleaf) – simple form posting to /login with hidden backurl field.

Web System (Client) Demo

SSO Filter – intercepts requests, checks local session; if missing, uses ticket parameter to fetch user from Redis, populates local session, deletes ticket.

public class SSOFilter implements Filter {
    private RedisTemplate redisTemplate;
    public static final String USER_INFO = "user";

    public SSOFilter(RedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        Object userInfo = request.getSession().getAttribute(USER_INFO);
        String requestUrl = request.getServletPath();
        if (!"/toLogin".equals(requestUrl) && !requestUrl.startsWith("/login") && userInfo == null) {
            String ticket = request.getParameter("ticket");
            if (ticket != null) {
                userInfo = redisTemplate.opsForValue().get(ticket);
            }
            if (userInfo == null) {
                response.sendRedirect("http://127.0.0.1:8080/toLogin?url=" + request.getRequestURL().toString());
                return;
            }
            UserForm user = (UserForm) userInfo;
            request.getSession().setAttribute(SSOFilter.USER_INFO, user);
            redisTemplate.delete(ticket);
        }
        chain.doFilter(request, response);
    }
    // init, destroy omitted
}

Client Controller – reads user from session and renders index page.

@Controller
public class IndexController {
    @Autowired
    private RedisTemplate redisTemplate;

    @GetMapping("/index")
    public ModelAndView index(HttpServletRequest request) {
        ModelAndView mv = new ModelAndView();
        Object userInfo = request.getSession().getAttribute(SSOFilter.USER_INFO);
        UserForm user = (UserForm) userInfo;
        mv.setViewName("index");
        mv.addObject("user", user);
        request.getSession().setAttribute("test", "123");
        return mv;
    }
}

Client Index Page (Thymeleaf) – displays welcome message with username.

CAS vs OAuth2

OAuth2 : Third-party authorization protocol. Allows a client to access a user's resources on a resource server without sharing credentials. Protects server-side user resources.

CAS (Central Authentication Service) : SSO framework based on Kerberos tickets. Provides reliable single sign-on for web applications. Protects client-side user resources (i.e., ensures the user has permission to access the client application).

Use CAS when you need unified username/password authentication across your own systems. Use OAuth2 when you need to authorize third-party applications to access your users' data.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Distributed SystemsRedisSpring BootAuthenticationCASSession ManagementSSOSingle Sign-On
Architect's Guide
Written by

Architect's Guide

Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.