Information Security 17 min read

Implementing Single Sign‑On (SSO) with CAS and Session Sharing in Distributed Systems

This article explains the problems of traditional session management in multi‑service environments, introduces session replication and centralized storage (using Redis), and demonstrates a complete CAS‑based single sign‑on solution with Java code examples for user, controller, filter, and configuration components.

Top Architect
Top Architect
Top Architect
Implementing Single Sign‑On (SSO) with CAS and Session Sharing in Distributed Systems

Background

When a product matrix becomes large, users have to log in to multiple systems, which leads to poor user experience and increased password management costs. A unified authentication mechanism (single sign‑on) can greatly improve efficiency.

Traditional Session Mechanism

HTTP is a stateless protocol, so servers create a new session for each request. The session identifier (e.g., JSESSIONID ) is stored in a cookie or URL rewrite. The server keeps session data in memory, keyed by the session ID.

Typical workflow:

Server reads the cookie value (session ID).

Looks up session data from the server‑side store.

If the session ID does not exist, a new session is created and the ID is sent back to the client.

Session Problems in Clustered Environments

In a distributed deployment, load balancers may route consecutive requests from the same user to different servers. Because the session is stored locally on each server, the second request may not find the original session, causing authentication failures.

Session Sharing Solutions

Two main approaches are used:

Session replication : copy session data to all nodes whenever it changes. This approach has high cost and latency.

Centralized session storage : store sessions in an external service (commonly Redis) so every node reads/writes the same data.

Multi‑Service Login Dilemma and SSO

Enterprises with many subsystems traditionally require separate logins for each system, which is cumbersome for users. Single sign‑on (SSO) solves this by allowing a user to log in once and obtain a ticket that is accepted by all downstream services.

CAS (Central Authentication Service) Architecture

CAS provides a domain‑wide login service. The typical flow is:

User accesses a protected service (e.g., b.com ) and is redirected to the CAS domain ( ouath.com ).

User logs in on the CAS login page.

CAS creates a ticket and stores <ticket, sessionId> in Redis, then redirects back to the original service.

The service receives the ticket, retrieves the session ID from Redis, creates a local session, and grants access.

Code Demonstration

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 omitted for brevity
}

Login controller (simplified):

@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;
        }
        // populate demo user and show login page
        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 {
        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);
        }
    }
}

Login filter (checks session, redirects to login if missing):

public class LoginFilter implements Filter {
    public static final String USER_INFO = "user";
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        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;
        }
        filterChain.doFilter(request, servletResponse);
    }
}

SSO filter (centralized session retrieval using Redis and 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 servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        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());
                return;
            }
            request.getSession().setAttribute(USER_INFO, userInfo);
            redisTemplate.delete(ticket);
        }
        filterChain.doFilter(request, servletResponse);
    }
}

CAS vs OAuth2

CAS is a web‑SSO framework that authenticates users and issues tickets for resource access, focusing on client‑side security. OAuth2 is an authorization protocol that allows third‑party applications to access a resource owner’s data without exposing credentials, focusing on server‑side resource protection.

Conclusion

The article provides a complete walkthrough of why traditional session mechanisms fail in distributed systems, how to share sessions via replication or centralized storage, and how to implement a CAS‑based SSO solution with Java, Spring MVC, and Redis. It also clarifies the distinction between CAS and OAuth2 for authentication versus authorization.

backendJavaRedisAuthenticationCASSSOsession
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

0 followers
Reader feedback

How this landed with the community

login 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.