introduction
Single Sign-On (SSO) is a common user authentication scheme in enterprise application systems. It allows users to access multiple related but independent systems using a set of credentials without repeated logins. For enterprises with multiple applications, SSO can significantly improve user experience and reduce credential management costs.
1. Traditional SSO solution based on Cookie-Session
principle
This is the most basic SSO implementation method. Its core is to store the user authentication status in the server session and save the Session identifier on the client through cookies. When the user logs into the SSO server, the server creates a session to store user information and sets the SessionID under the top-level domain name through a cookie, so that all subdomain applications can access the cookie and verify the same session.
Implementation plan
- Create an SSO server
@RestController @RequestMapping("/sso") public class SsoServerController { @Autowired private UserService userService; @PostMapping("/login") public ResponseEntity<String> login( @RequestParam String username, @RequestParam String password, HttpServletRequest request, HttpServletResponse response, @RequestParam(required = false) String redirect) { // Verify user credentials User user = (username, password); if (user == null) { return ().body("Invalid credentials"); } // Create a session and store user information HttpSession session = (true); ("USER_INFO", user); (3600); // Session valid for 1 hour // Get SessionID String sessionId = (); // Set cookies Cookie cookie = new Cookie("SSO_SESSION_ID", sessionId); (3600); // 1 hour valid ("/"); ("."); // Key: Top-level domains, making all subdomains accessible (true); (true); // Transfer via HTTPS only (cookie); // If there is a redirect URL, redirect back to the original application if (redirect != null && !()) { return () .header("Location", redirect) .build(); } return ("Login successful"); } @GetMapping("/validate") public ResponseEntity<UserInfo> validateSession( @CookieValue(name = "SSO_SESSION_ID", required = false) String sessionId) { if (sessionId == null) { return ().build(); } // Verify the validity of the Session and obtain user information HttpSession session = (sessionId); if (session == null) { return ().build(); } // Get user information from Session User user = (User) ("USER_INFO"); if (user == null) { return ().build(); } // Return user information UserInfo userInfo = new UserInfo((), (), ()); return (userInfo); } @PostMapping("/logout") public ResponseEntity<Void> logout( @CookieValue(name = "SSO_SESSION_ID", required = false) String sessionId, HttpServletResponse response, @RequestParam(required = false) String redirect) { // Invalidate Session if (sessionId != null) { HttpSession session = (sessionId); if (session != null) { (); } } // Delete Cookies Cookie cookie = new Cookie("SSO_SESSION_ID", null); (0); ("/"); ("."); (cookie); // If there is a redirect URL, redirect if (redirect != null && !()) { return () .header("Location", redirect) .build(); } return ().build(); } }
- Client application integration
@Component public class SsoFilter implements Filter { @Autowired private RestTemplate restTemplate; private static final String SSO_SERVER_URL = ""; private static final String SSO_VALIDATION_URL = SSO_SERVER_URL + "/sso/validate"; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; // Check whether the current application already has a local session HttpSession currentSession = (false); if (currentSession != null && ("USER_INFO") != null) { // There is already a local session, continue to request (request, response); return; } // Get SSO Session Cookies Cookie[] cookies = (); String ssoSessionId = null; if (cookies != null) { for (Cookie cookie : cookies) { if ("SSO_SESSION_ID".equals(())) { ssoSessionId = (); break; } } } // SSO cookie is not found, redirect to SSO login page if (ssoSessionId == null) { String redirectUrl = SSO_SERVER_URL + "/login?redirect=" + (().toString(), "UTF-8"); (redirectUrl); return; } // Verify the validity of SSO Session try { HttpHeaders headers = new HttpHeaders(); ("Cookie", "SSO_SESSION_ID=" + ssoSessionId); HttpEntity<String> entity = new HttpEntity<>(headers); ResponseEntity<UserInfo> responseEntity = ( SSO_VALIDATION_URL, , entity, ); if (() == ) { // SSO Session is valid, create a local Session UserInfo userInfo = (); HttpSession session = (true); ("USER_INFO", userInfo); (request, response); } else { // SSO Session is invalid, redirect to login page String redirectUrl = SSO_SERVER_URL + "/login?redirect=" + (().toString(), "UTF-8"); (redirectUrl); } } catch (Exception e) { // There was an error in the verification process, redirect to the login page String redirectUrl = SSO_SERVER_URL + "/login?redirect=" + (().toString(), "UTF-8"); (redirectUrl); } } }
Pros and cons
advantage:
- The implementation is relatively simple and follows the traditional web development model
- The server fully controls the session state and life cycle
- Clients do not need to store and manage complex states
- Support instant session failure and revocation
shortcoming:
- Restricted by the same origin policy, only applicable to applications under the same top-level domain name
- Relying on cookie mechanisms may be restricted in certain environments (such as mobile applications)
- There is a risk of CSRF
2. Stateless SSO scheme based on JWT
principle
JWT (JSON Web Token) is a compact, self-contained token format that can safely pass information between different applications. When implementing SSO using JWT, the authentication server generates a JWT token after the user logs in, which contains user-related information and signatures. Since JWT can be independently verified without querying a central server, it is ideal for building stateless SSO systems.
Implementation plan
- Create a JWT certification service
First add dependencies:
<dependency> <groupId></groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId></groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId></groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency>
Implement JWT tool class:
@Component public class JwtUtil { @Value("${}") private String secretKey; @Value("${}") private long expirationTime; @PostConstruct protected void init() { secretKey = ().encodeToString(()); } public String generateToken(User user) { Map<String, Object> claims = new HashMap<>(); ("id", ()); ("username", ()); ("email", ()); ("roles", ()); return createToken(claims, ()); } private String createToken(Map<String, Object> claims, String subject) { Date now = new Date(); Date expiryDate = new Date(() + expirationTime); return () .setClaims(claims) .setSubject(subject) .setIssuedAt(now) .setExpiration(expiryDate) .signWith((()), SignatureAlgorithm.HS512) .compact(); } public boolean validateToken(String token) { try { () .setSigningKey((())) .build() .parseClaimsJws(token); return true; } catch (Exception e) { return false; } } public Claims extractAllClaims(String token) { return () .setSigningKey((())) .build() .parseClaimsJws(token) .getBody(); } }
Implement authentication controller:
@RestController @RequestMapping("/auth") public class AuthController { @Autowired private UserService userService; @Autowired private JwtUtil jwtUtil; @PostMapping("/login") public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) { // Verify user credentials User user = ( (), () ); if (user == null) { return () .body(new ErrorResponse("Invalid credentials")); } // Generate JWT String jwt = (user); // Set refresh token (optional) String refreshToken = ().toString(); ((), refreshToken, 30); // 30-day validity period // Return to Token return (new JwtResponse(jwt, refreshToken)); } @PostMapping("/refresh") public ResponseEntity<?> refreshToken(@RequestBody RefreshTokenRequest request) { String refreshToken = (); // Verify refresh token User user = (refreshToken); if (user == null) { return () .body(new ErrorResponse("Invalid refresh token")); } // Generate a new JWT String newToken = (user); return (new JwtResponse(newToken, refreshToken)); } @PostMapping("/validate") public ResponseEntity<?> validateToken(@RequestBody TokenValidationRequest request) { String token = (); if (!(token)) { return () .body(new ErrorResponse("Invalid token")); } // Extract user information Claims claims = (token); Map<String, Object> userInfo = new HashMap<>(claims); return (userInfo); } @PostMapping("/logout") public ResponseEntity<?> logout(@RequestBody LogoutRequest request) { // Delete the refresh token (()); // JWT itself cannot be revoked, the client needs to discard the token return (new SuccessResponse("Logout successful")); } }
- Client application integration
JWT filter:
@Component public class JwtRequestFilter extends OncePerRequestFilter { @Autowired private JwtUtil jwtUtil; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { final String authorizationHeader = ("Authorization"); String username = null; String jwt = null; // Extract JWT from Authorization header if (authorizationHeader != null && ("Bearer ")) { jwt = (7); try { // Verify the token and extract user information if ((jwt)) { Claims claims = (jwt); username = (); // Build authentication objects List<String> roles = (List<String>) ("roles"); List<GrantedAuthority> authorities = () .map(SimpleGrantedAuthority::new) .collect(()); UserDetails userDetails = new User(username, "", authorities); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, ()); (new WebAuthenticationDetailsSource().buildDetails(request)); ().setAuthentication(authentication); } } catch (Exception e) { // JWT verification failed, authentication information is not set } } (request, response); } }
Security configuration:
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private JwtRequestFilter jwtRequestFilter; @Override protected void configure(HttpSecurity http) throws Exception { ().disable() .authorizeRequests() .antMatchers("/api/public/**", "/auth/login", "/auth/refresh").permitAll() .anyRequest().authenticated() .and() .sessionManagement() .sessionCreationPolicy(); (jwtRequestFilter, ); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return (); } }
- Front-end processing JWT
//Storage the JWT token after login is successfulfunction handleLoginSuccess(response) { const { token, refreshToken } = ; ('jwtToken', token); ('refreshToken', refreshToken); // Set the default Authorization header ['Authorization'] = `Bearer ${token}`; // Redirect to the application page = '/dashboard'; } // Add request interceptor to automatically attach token(config => { const token = ('jwtToken'); if (token) { = `Bearer ${token}`; } return config; }); // Add a response interceptor to handle the expiration of tokens( response => response, async error => { const originalRequest = ; // If it is a 401 error and is not a refresh token request, try refreshing the token if ( === 401 && !originalRequest._retry) { originalRequest._retry = true; try { const refreshToken = ('refreshToken'); const response = await ('/auth/refresh', { refreshToken }); const { token } = ; ('jwtToken', token); ['Authorization'] = `Bearer ${token}`; return axios(originalRequest); } catch (err) { // Refresh failed, redirect to the login page ('jwtToken'); ('refreshToken'); = '/login'; return (err); } } return (error); } );
Pros and cons
advantage:
- Completely stateless, the server does not need to store session information
- Cross-domain support for distributed systems and microservices
- Good scalability, JWT can contain rich user information
- Don't rely on cookies to avoid CSRF problems
- Suitable for various clients (Web, mobile applications, APIs)
shortcoming:
- Unable to proactively invalidate issued tokens (unless blacklisting mechanism is used)
- JWT may be larger, increasing the network transmission burden
- Token management requires client intervention
- Refresh token mechanism is more complex
- There is a risk of tokens being stolen
3. SSO solution based on OAuth 2.0/OpenID Connect
principle
OAuth 2.0 is an authorization framework, while OpenID Connect (OIDC) is an authentication layer built on OAuth 2.0. This is the most standardized and perfect SSO solution at present, especially suitable for enterprise-level applications and scenarios where third-party integration is required. It provides a wealth of authorization process options and security features.
Implementation plan
In SpringBoot, Spring Security OAuth2 can be used to implement OAuth 2.0/OIDC servers and clients.
- Build an authorized server
First add dependencies:
<dependency> <groupId></groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> <version>2.6.8</version> </dependency> <dependency> <groupId></groupId> <artifactId>spring-security-oauth2-jose</artifactId> </dependency>
Configure the authorization server:
@Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private AuthenticationManager authenticationManager; @Autowired private PasswordEncoder passwordEncoder; @Autowired private UserDetailsService userDetailsService; @Autowired private DataSource dataSource; @Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); ("your-signing-key"); return converter; } @Bean public TokenStore tokenStore() { return new JwtTokenStore(accessTokenConverter()); } @Bean public TokenEnhancer tokenEnhancer() { return new CustomTokenEnhancer(); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) { TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain(); ( (tokenEnhancer(), accessTokenConverter())); endpoints .tokenStore(tokenStore()) .tokenEnhancer(tokenEnhancerChain) .authenticationManager(authenticationManager) .userDetailsService(userDetailsService) .reuseRefreshTokens(false); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { (dataSource) .withClient("web-client") .secret(("web-client-secret")) .authorizedGrantTypes("password", "refresh_token", "authorization_code") .scopes("read", "write") .redirectUris("/login/oauth2/code/custom", "/login/oauth2/code/custom") .accessTokenValiditySeconds(3600) .refreshTokenValiditySeconds(86400) .and() .withClient("mobile-client") .secret(("mobile-client-secret")) .authorizedGrantTypes("password", "refresh_token") .scopes("read") .accessTokenValiditySeconds(7200) .refreshTokenValiditySeconds(259200); } @Override public void configure(AuthorizationServerSecurityConfigurer security) { security .tokenKeyAccess("permitAll()") .checkTokenAccess("isAuthenticated()") .allowFormAuthenticationForClients(); } } // Custom token enhancer to add additional user informationpublic class CustomTokenEnhancer implements TokenEnhancer { @Override public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { if (() instanceof UserDetails) { UserDetails userDetails = (UserDetails) (); Map<String, Object> additionalInfo = new HashMap<>(); // Add additional user information if (userDetails instanceof CustomUserDetails) { CustomUserDetails customUserDetails = (CustomUserDetails) userDetails; ("userId", ()); ("email", ()); ("fullName", ()); } ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo); } return accessToken; } }
Configure the resource server:
@Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { () .antMatchers("/api/public/**").permitAll() .antMatchers("/api/user/**").hasRole("USER") .antMatchers("/api/admin/**").hasRole("ADMIN") .anyRequest().authenticated(); } }
- Client application integration
Client configuration():
spring: security: oauth2: client: registration: custom: client-id: web-client client-secret: web-client-secret authorization-grant-type: authorization_code redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" scope: read,write provider: custom: authorization-uri: /oauth/authorize token-uri: /oauth/token user-info-uri: /userinfo jwk-set-uri: /.well-known/ user-name-attribute: sub
Client security configuration:
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests(authorizeRequests -> authorizeRequests .antMatchers("/", "/login/**", "/error", "/webjars/**").permitAll() .anyRequest().authenticated() ) .oauth2Login(oauth2Login -> oauth2Login .loginPage("/login") .defaultSuccessUrl("/home") .userInfoEndpoint() .userService(oAuth2UserService()) ) .logout(logout -> logout .logoutSuccessUrl("/logout?client_id=web-client") .invalidateHttpSession(true) .clearAuthentication(true) .deleteCookies("JSESSIONID") ); } @Bean public OAuth2UserService<OAuth2UserRequest, OAuth2User> oAuth2UserService() { DefaultOAuth2UserService delegate = new DefaultOAuth2UserService(); return userRequest -> { OAuth2User oAuth2User = (userRequest); // Customize user information processing Map<String, Object> attributes = (); String userNameAttributeName = () .getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); return new DefaultOAuth2User( (), attributes, userNameAttributeName); }; } }
- Complete logout process
@Controller public class AuthController { @GetMapping("/logout") public String logout(HttpServletRequest request, HttpServletResponse response, @RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient) { // Clear local session (); HttpSession session = (false); if (session != null) { (); } // Clear cookies Cookie[] cookies = (); if (cookies != null) { for (Cookie cookie : cookies) { (""); ("/"); (0); (cookie); } } // Redirect to the logout endpoint of the OAuth2 server String logoutUrl = "/logout?client_&post_logout_redirect_uri=" + ("/", StandardCharsets.UTF_8); return "redirect:" + logoutUrl; } }
Pros and cons
advantage:
- Mature security protocols, widely adopted industry standards
- Supports multiple authentication processes (authorization code, implicit, password, etc.)
- Token revocation mechanism is improved
- Excellent scalability, suitable for enterprise-level applications
- Clarify the separation of certification and authorization responsibilities
shortcoming:
- High complexity in implementation, small applications may not be suitable
- Steep learning curves for configuration and understanding
4. Shared session SSO solution based on Spring Session
principle
Spring Session provides a framework for storing session data in shared external storage (such as Redis), allowing session information to be shared between different applications. This approach is particularly suitable for Spring-based isomorphic systems, which can solve distributed session sharing problems while keeping simple implementations.
Implementation plan
- Add dependencies
<dependency> <groupId></groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId></groupId> <artifactId>spring-session-data-redis</artifactId> </dependency>
- Configuring Spring Session
@Configuration @EnableRedisHttpSession public class SessionConfig { @Bean public LettuceConnectionFactory connectionFactory() { // Redis connection configuration RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration(); ("redis-host"); (6379); return new LettuceConnectionFactory(redisConfig); } @Bean public CookieSerializer cookieSerializer() { DefaultCookieSerializer serializer = new DefaultCookieSerializer(); ("SESSION"); // Use a unified cookie name ("/"); ("^.+?\.(\w+\.[a-z]+)$"); // Support subdomains (true); (true); return serializer; } @Bean public HttpSessionIdResolver httpSessionIdResolver() { return new CookieHttpSessionIdResolver(); } }
- Create a central certification service
@Controller @RequestMapping("/auth") public class CentralAuthController { @Autowired private UserService userService; @GetMapping("/login") public String loginPage(@RequestParam(required = false) String redirect, Model model) { ("redirectUrl", redirect); return "login"; } @PostMapping("/login") public String login(@RequestParam String username, @RequestParam String password, @RequestParam(required = false) String redirect, HttpSession session, RedirectAttributes redirectAttrs) { // Verify user credentials User user = (username, password); if (user == null) { ("error", "Invalid credentials"); return "redirect:/auth/login"; } // Save user information into a shared session UserInfo userInfo = new UserInfo((), (), ()); ("USER_INFO", userInfo); // If there is a redirect URL, redirect back to the original application if (redirect != null && !()) { return "redirect:" + redirect; } return "redirect:/dashboard"; } @GetMapping("/logout") public String logout(HttpServletRequest request, HttpSession session) { // Invalidate the session (); // Optional: Get the URL to be redirected String referer = ("Referer"); return "redirect:/auth/login"; } }
- Create in-app authentication filters
@Component public class SessionAuthenticationFilter implements Filter { private static final String LOGIN_PAGE = "/auth/login"; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; // No authentication is required for public path String path = (); if (isPublicPath(path)) { (request, response); return; } // Check whether there is user information in the session HttpSession session = (false); boolean authenticated = session != null && ("USER_INFO") != null; if (authenticated) { // The user has been authenticated and continue to request (request, response); } else { // Not authenticated, redirect to login page String redirectUrl = LOGIN_PAGE + "?redirect=" + (().toString(), "UTF-8"); (redirectUrl); } } private boolean isPublicPath(String path) { return ("/public/") || ("/resources/") || ("/error"); } }
- Configure in-app WebMVC
@Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private SessionAuthenticationFilter sessionAuthenticationFilter; @Bean public FilterRegistrationBean<SessionAuthenticationFilter> sessionFilterRegistration() { FilterRegistrationBean<SessionAuthenticationFilter> registration = new FilterRegistrationBean<>(); (sessionAuthenticationFilter); ("/*"); (Ordered.HIGHEST_PRECEDENCE + 1); // Execute after Spring Security return registration; } }
- Use session information
@Controller @RequestMapping("/dashboard") public class DashboardController { @GetMapping public String dashboard(HttpSession session, Model model) { UserInfo userInfo = (UserInfo) ("USER_INFO"); ("user", userInfo); return "dashboard"; } }
Pros and cons
advantage:
- Simple to implement and easy to understand
- Seamless integration with Spring Eco
- Sessions can contain rich information
shortcoming:
- Rely on central storage (such as Redis)
- Session data needs to be serialized/deserialized
- Relying on cookies is not suitable for non-web applications
5. Plan selection and best practices
Select a suggestion
Program Type | Recommended scenarios | Not suitable for scenes |
---|---|---|
Cookie-Session | Small applications under the same domain name, simple authentication requirements | Cross-domain applications, mobile application integration, high security requirements |
JWT | Distributed microservices, front-end and back-end separation applications | Scenarios where tokens need to be revoked instantly, with extremely high security requirements |
OAuth 2.0/OIDC | Enterprise-level applications, requiring third-party integration, multi-tenant system | Small applications, resource-constrained environment, rapid development needs |
Spring Session | Application of Spring Technology Stack, Application of Medium Enterprises | Heterogeneous technology stack, non-Web application integration |
6. Summary
From a simple Cookie-Session-based solution to a complex OAuth 2.0/OIDC implementation, the choice of SSO solution should be comprehensively considered based on business needs, security requirements, user experience goals and technical constraints.
The above is the detailed content of the four solutions for SpringBoot to implement single sign-on (SSO). For more information about SpringBoot single sign-on SSO, please follow my other related articles!