Mastering Single Sign-On: From Session Basics to CAS Implementation in Java
This article explains the limitations of traditional HTTP session handling, explores session sharing strategies for clustered environments, and walks through a complete Java CAS‑based single sign‑on solution with code examples, highlighting the differences between CAS and OAuth2.
Background
When a company has many products, users must switch between systems, which hurts experience and raises password‑management costs. Implementing a unified authentication (single sign‑on) can greatly improve usability and security.
Traditional Session Mechanism and Authentication
Cookie Interaction
HTTP is stateless, so servers create a new session for each request. The session ID (JSESSIONID) is stored in a cookie or URL rewrite, allowing the server to associate requests with a specific user.
JSESSIONID is kept in browser memory; if cookies are disabled, the ID appears in the URL. Different browser windows receive different session IDs, preventing cross‑window sharing.
Server‑Side Session Mechanism
The server checks incoming requests for a JSESSIONID. If present, it retrieves the session from memory; otherwise it creates a new session and returns the ID to the client.
Server looks up the cookie value (sessionId).
Retrieves session data from server‑side storage.
If not found, creates a new session and writes a cookie to the response.
Session‑Based Authentication Flow
Because HTTP is stateless, most web authentication relies on this session pattern, which has inherent drawbacks.
Session Challenges in Clustered Environments and Solutions
With growing traffic, applications are deployed on multiple servers behind a load balancer, causing a user's requests to hit different nodes. Since sessions are stored on a single server, the next request may miss the session.
Session Sharing Strategies
Two main approaches
Session replication – copy session data to all nodes.
Session centralized storage – store sessions in a shared service (e.g., Redis).
Session replication copies session data on login, update, or logout, but it is costly, hard to maintain, and can introduce latency.
Session centralized storage places session data in a single service (commonly Redis), avoiding synchronization overhead.
Login Challenges in Multi‑Service Environments and SSO Solutions
Why SSO Emerged
Enterprises with many systems require users to log in separately to each, leading to poor management and user experience. Single sign‑on lets a user authenticate once and access all systems.
CAS Underlying Principle
For different domains, cookies cannot be shared, so a dedicated domain (e.g., auth.com) issues a ticket that links to a session ID stored in Redis.
When a protected site (b.com) is accessed, it redirects to auth.com for login.
User logs in at auth.com; a ticket is generated and stored with the session ID in Redis.
b.com receives the ticket, retrieves the session ID from Redis, creates a local session, and redirects the user back.
CAS vs. OAuth2
OAuth2 is a third‑party authorization protocol that lets a client access resources without the user sharing credentials.
CAS (Central Authentication Service) is a web‑centric SSO framework that authenticates users for multiple web applications.
CAS focuses on verifying a user's access to a client’s resources, while OAuth2 protects the resource server’s data.
CAS Demo Code
User entity class:
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, 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().length() == 0) {
response.sendRedirect("/index");
} else {
response.sendRedirect(user.getBackurl() + "?ticket=" + ticket);
}
}
}Login filter:
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);
}
// init and destroy omitted
}SSO filter (centralized session storage):
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().toString());
return;
}
request.getSession().setAttribute(USER_INFO, userInfo);
redisTemplate.delete(ticket);
}
filterChain.doFilter(request, servletResponse);
}
// init and destroy omitted
}With these components, a Java web application can provide seamless single sign‑on across multiple services.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java High-Performance Architecture
Sharing Java development articles and resources, including SSM architecture and the Spring ecosystem (Spring Boot, Spring Cloud, MyBatis, Dubbo, Docker), Zookeeper, Redis, architecture design, microservices, message queues, Git, etc.
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.
