Mastering Single Sign-On: From Session Basics to CAS Implementation

This article walks through the fundamentals of HTTP session handling, the challenges of session sharing in clustered environments, and presents a step‑by‑step design of a Single Sign‑On solution using CAS, including concrete code examples, Redis‑based session storage, and a comparison with OAuth2.

Architect
Architect
Architect
Mastering Single Sign-On: From Session Basics to CAS Implementation

Problem: Multiple independent systems require separate logins

When a product portfolio grows, users must log in to each system separately, which degrades user experience and increases password‑management overhead. A unified authentication mechanism—Single Sign‑On (SSO)—is needed.

Traditional HTTP session mechanism

Cookie‑based JSESSIONID

HTTP is stateless, so the server creates a new thread for each request. To associate requests with the same user, the server generates a JSESSIONID cookie and stores it in the browser memory. If cookies are disabled, the session ID is appended to the URL (e.g., sessionid=KWJHUG6JJM65HS2K6).

Server‑side session handling

Server reads the cookie value (sessionId).

Looks up session data in an in‑memory hash table.

If not found, creates a new session, generates a cookie, and writes it to the response header.

Authentication flow

Client sends a request → server checks for a valid session → if absent, redirects to a login page → after successful login, server creates a session and returns the JSESSIONID cookie. Subsequent requests carry this cookie, allowing the server to identify the user.

Session sharing challenges in a clustered deployment

In a distributed deployment, a load balancer 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 loss.

Session replication

Each server copies session data to the others on login, update, or logout. This approach incurs high implementation cost, maintenance difficulty, and latency.

Centralized session store (Redis)

All servers read and write session data to a shared Redis store, eliminating synchronization issues and providing a single source of truth for session state.

Single Sign‑On with CAS

CAS login flow (step‑by‑step)

Client accesses b.com without a login; b.com redirects to the central domain ouath.com.

User logs in on ouath.com; a cookie is set for ouath.com.

The backend stores <ticket, sessionId> in Redis and redirects back to the original system.

When b.com receives the request with the ticket, it looks up the sessionId in Redis, synchronizes its own session, sets its own cookie, and redirects to the original page.

The request now carries a valid cookie; the backend validates the login state successfully.

CAS vs. OAuth2

OAuth2 is a third‑party authorization protocol that lets a client access resources without exposing user credentials; it protects server‑side resources. CAS is a web‑SSO framework that authenticates users centrally; it protects client‑side resource access. Use CAS when a unified login is required, and OAuth2 when delegated authorization to third‑party services is needed.

Demo implementation (Spring MVC + Redis)

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

@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 {
        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();
        UserForm user = (UserForm) request.getSession().getAttribute(LoginFilter.USER_INFO);
        mv.setViewName("index");
        mv.addObject("user", user);
        return mv;
    }
}

Login filter

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) {
            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);
        }
        chain.doFilter(request, response);
    }
    // init and destroy omitted
}

SSO filter (alternative implementation)

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());
                return;
            }
            request.getSession().setAttribute(USER_INFO, userInfo);
            redisTemplate.delete(ticket);
        }
        chain.doFilter(request, res);
    }
    // init and destroy omitted
}

Thymeleaf login page

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>enjoy login</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
</head>
<body>
<div style="text-align:center">
    <h1>Please Log In</h1>
    <form th:action="@{/login}" th:object="${user}" method="post">
        <p>Username: <input type="text" th:field="*{username}"/></p>
        <p>Password: <input type="text" th:field="*{password}"/></p>
        <p><input type="submit" value="Submit"/> <input type="reset" value="Reset"/></p>
        <input type="hidden" th:field="*{backurl}"/>
    </form>
</div>
</body>
</html>

Conclusion

The transition from a simple cookie‑based session model to a robust SSO architecture uses CAS for centralized authentication and Redis as a shared session store, solving the loss‑of‑session problem in clustered environments. The comparison with OAuth2 clarifies that CAS secures client‑side login while OAuth2 handles delegated authorization of server‑side resources, guiding architects to choose the appropriate protocol for their ecosystem.

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.

JavaredisspringAuthenticationCASSSOSession
Architect
Written by

Architect

Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.

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.