Implementing Single Sign-On (SSO) with CAS and Session Management in Java
This article explains traditional session mechanisms, challenges in clustered environments, and presents solutions such as session replication and centralized storage, then details the design and implementation of a CAS-based Single Sign-On system in Java, including code for user forms, controllers, filters, and configuration.
When a product matrix grows, users must log in to each system separately, causing poor experience and security issues. This article introduces traditional session mechanisms, their limitations in clustered environments, and presents solutions such as session replication and centralized storage using Redis.
Traditional Session Mechanism and Identity Authentication
HTTP is stateless; servers create a session identified by JSESSIONID stored in a cookie or URL. If the cookie is disabled, the session ID appears in the URL as sessionid=... . Sessions are stored in server memory and are not shared across browsers or servers.
Session Processing Flow
Server checks for the session cookie value.
Uses the session ID to retrieve session data from storage.
If not found, creates a new session and returns the cookie.
Session Sharing in Clustered Environments
Two main approaches are session replication and centralized storage. Replication copies session data to all nodes, which is costly and may cause latency. Centralized storage places session data in a single service, typically Redis, allowing all nodes to access the same session.
Single Sign-On (SSO) with CAS
CAS (Central Authentication Service) provides a ticket‑based SSO solution that works across different domains. The workflow includes redirecting unauthenticated requests to ouath.com , logging in, storing a ticket‑session mapping in Redis, and redirecting back with the ticket to obtain the session.
Key Code Implementation
UserForm.java
public class UserForm implements Serializable { private static final long serialVersionUID = 1L; private String username; private String password; private String backurl; // getters and setters }IndexController.java (CAS login demo)
@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().length() == 0) { 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); return mv; } }LoginFilter.java
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); } }LoginConfig.java (filter registration)
@Configuration public class LoginConfig { @Bean public FilterRegistrationBean sessionFilterRegistration() { FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setFilter(new LoginFilter()); registration.addUrlPatterns("/*"); registration.setName("sessionFilter"); registration.setOrder(1); return registration; } }Login page (Thymeleaf template)
<!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 text-align="center"><h1>请登陆</h1><form th:action="@{/login}" th:object="${user}" method="post"><p>用户名: <input type="text" th:field="*{username}"/></p><p>密码: <input type="text" th:field="*{password}"/></p><p><input type="submit" value="Submit"/><input type="reset" value="Reset"/></p><input type="text" th:field="*{backurl}" hidden="hidden"/></form></div></body></html>SSOFilter.java (centralized session handling)
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(USER_INFO, user); redisTemplate.delete(ticket); } chain.doFilter(request, response); } }The article also compares CAS‑based SSO with OAuth2, noting that CAS focuses on authenticating users for multiple client applications, while OAuth2 authorizes third‑party access to resources.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.