How to Build Single Sign‑On (SSO) with CAS and Session Sharing in Java
This article explains why multiple independent login systems hurt user experience and security, reviews traditional session mechanisms and their limitations in clustered environments, and then presents two session‑sharing strategies and a complete CAS‑based SSO solution with Java code examples.
Background
When a company has many products, users must switch between systems, which leads to poor experience and higher password‑management costs. The lack of a unified authentication also reduces overall product security.
Traditional Session Mechanism and Authentication Scheme
HTTP is a stateless protocol, so each request creates a new thread on the server without preserving client context. To identify a user across requests, a session is created on the server and a
JSESSIONIDis sent to the browser via a cookie (or URL rewriting if cookies are disabled).
Typical flow for each HTTP request:
The server looks for the cookie containing the session ID.
If found, it retrieves the session data from the server‑side store.
If not found, a new session is created and the session ID is written back as a cookie.
Sessions are stored in memory as hash tables on the server. Because the session ID is stored in the browser’s memory, it cannot be shared across different browser windows or across different servers in a cluster.
Session Sharing Problems in a Cluster
When traffic grows, applications are deployed on multiple servers behind a load balancer. A user’s first request may be handled by server A, creating a session there; a subsequent request may be routed to server B, which cannot find the session, resulting in a failed login state.
Session Sharing Solutions
Two common approaches are used:
Session replication : copy session data to all servers. This incurs high implementation cost and latency.
Centralized session storage : store sessions in a dedicated service (e.g., Redis) that all servers read from and write to, eliminating the need for replication.
Multi‑Service Login Dilemma and SSO Solution
Enterprises often have many independent systems, each with its own login page. Users must log in repeatedly, which is cumbersome. Single Sign‑On (SSO) solves this by allowing a user to log in once and gain access to all systems via a shared ticket.
CAS – The Underlying SSO Mechanism
CAS (Central Authentication Service) implements SSO using a ticket‑based workflow:
System A redirects the user to
ouath.comfor login.
User authenticates; CAS creates a ticket and stores
{ticket: sessionId}in Redis.
CAS redirects back to System A with the ticket.
System A retrieves the session ID from Redis using the ticket, creates a cookie, and grants access.
The overall interaction diagram is shown below:
CAS Implementation Demo (Java Spring)
User entity :
<code>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
}</code>Login controller (handles login and redirects with ticket):
<code>@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();
Object user = request.getSession().getAttribute(LoginFilter.USER_INFO);
mv.setViewName("index");
mv.addObject("user", user);
return mv;
}
}</code>Login filter (protects resources):
<code>public class LoginFilter implements Filter {
public static final String USER_INFO = "user";
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;
Object userInfo = request.getSession().getAttribute(USER_INFO);
String url = request.getServletPath();
if (!"/toLogin".equals(url) && !url.startsWith("/login") && userInfo == null) {
request.getRequestDispatcher("/toLogin").forward(request, response);
return;
}
chain.doFilter(request, response);
}
// init and destroy omitted
}</code>Filter registration (Spring configuration) :
<code>@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;
}
}</code>CAS SSO filter (centralized ticket handling) :
<code>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 resp, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;
Object userInfo = request.getSession().getAttribute(USER_INFO);
String url = request.getServletPath();
if (!"/toLogin".equals(url) && !url.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
}</code>CAS vs. OAuth2
OAuth2 is a third‑party authorization protocol that lets a client access resources without the user providing credentials directly.
CAS (Central Authentication Service) is a Kerberos‑style ticket system that provides web‑based SSO, focusing on authenticating the user once for multiple web applications.
In short, CAS secures the client‑side user identity, while OAuth2 secures server‑side resource access.
Choosing the right solution depends on whether you need a unified login (CAS) or delegated resource access for third‑party apps (OAuth2).
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
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.