SpringBoot實現(xiàn)單點登錄(SSO)的四種方案
引言
單點登錄(Single Sign-On,SSO)是企業(yè)應(yīng)用系統(tǒng)中常見的用戶認證方案,它允許用戶使用一組憑證訪問多個相關(guān)但獨立的系統(tǒng),無需重復(fù)登錄。對于擁有多個應(yīng)用的企業(yè)來說,SSO可以顯著提升用戶體驗并降低憑證管理成本。
一、基于Cookie-Session的傳統(tǒng)SSO方案
原理
這是最基礎(chǔ)的SSO實現(xiàn)方式,其核心是將用戶認證狀態(tài)存儲在服務(wù)端Session中,并通過Cookie在客戶端保存Session標識符。當用戶登錄SSO服務(wù)器后,服務(wù)器創(chuàng)建Session存儲用戶信息,并將SessionID通過Cookie設(shè)置在頂級域名下,使所有子域應(yīng)用都能訪問該Cookie并驗證同一個Session。
實現(xiàn)方案
- 創(chuàng)建SSO服務(wù)端
@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) { // 驗證用戶憑證 User user = userService.authenticate(username, password); if (user == null) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials"); } // 創(chuàng)建session并存儲用戶信息 HttpSession session = request.getSession(true); session.setAttribute("USER_INFO", user); session.setMaxInactiveInterval(3600); // Session有效期1小時 // 獲取SessionID String sessionId = session.getId(); // 設(shè)置Cookie Cookie cookie = new Cookie("SSO_SESSION_ID", sessionId); cookie.setMaxAge(3600); // 1小時有效期 cookie.setPath("/"); cookie.setDomain(".example.com"); // 關(guān)鍵:頂級域名,使所有子域都能訪問 cookie.setHttpOnly(true); cookie.setSecure(true); // 僅通過HTTPS傳輸 response.addCookie(cookie); // 如果有重定向URL,則重定向回原應(yīng)用 if (redirect != null && !redirect.isEmpty()) { return ResponseEntity.status(HttpStatus.FOUND) .header("Location", redirect) .build(); } return ResponseEntity.ok("Login successful"); } @GetMapping("/validate") public ResponseEntity<UserInfo> validateSession( @CookieValue(name = "SSO_SESSION_ID", required = false) String sessionId) { if (sessionId == null) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } // 驗證Session有效性并獲取用戶信息 HttpSession session = sessionRegistry.getSession(sessionId); if (session == null) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } // 從Session獲取用戶信息 User user = (User) session.getAttribute("USER_INFO"); if (user == null) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } // 返回用戶信息 UserInfo userInfo = new UserInfo(user.getId(), user.getUsername(), user.getRoles()); return ResponseEntity.ok(userInfo); } @PostMapping("/logout") public ResponseEntity<Void> logout( @CookieValue(name = "SSO_SESSION_ID", required = false) String sessionId, HttpServletResponse response, @RequestParam(required = false) String redirect) { // 使Session失效 if (sessionId != null) { HttpSession session = sessionRegistry.getSession(sessionId); if (session != null) { session.invalidate(); } } // 刪除Cookie Cookie cookie = new Cookie("SSO_SESSION_ID", null); cookie.setMaxAge(0); cookie.setPath("/"); cookie.setDomain(".example.com"); response.addCookie(cookie); // 如果有重定向URL,則重定向 if (redirect != null && !redirect.isEmpty()) { return ResponseEntity.status(HttpStatus.FOUND) .header("Location", redirect) .build(); } return ResponseEntity.ok().build(); } }
- 客戶端應(yīng)用集成
@Component public class SsoFilter implements Filter { @Autowired private RestTemplate restTemplate; private static final String SSO_SERVER_URL = "https://sso.example.com"; 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; // 檢查當前應(yīng)用是否已有本地Session HttpSession currentSession = httpRequest.getSession(false); if (currentSession != null && currentSession.getAttribute("USER_INFO") != null) { // 已有本地Session,繼續(xù)請求 chain.doFilter(request, response); return; } // 獲取SSO Session Cookie Cookie[] cookies = httpRequest.getCookies(); String ssoSessionId = null; if (cookies != null) { for (Cookie cookie : cookies) { if ("SSO_SESSION_ID".equals(cookie.getName())) { ssoSessionId = cookie.getValue(); break; } } } // 未找到SSO Cookie,重定向到SSO登錄頁 if (ssoSessionId == null) { String redirectUrl = SSO_SERVER_URL + "/login?redirect=" + URLEncoder.encode(httpRequest.getRequestURL().toString(), "UTF-8"); httpResponse.sendRedirect(redirectUrl); return; } // 驗證SSO Session有效性 try { HttpHeaders headers = new HttpHeaders(); headers.add("Cookie", "SSO_SESSION_ID=" + ssoSessionId); HttpEntity<String> entity = new HttpEntity<>(headers); ResponseEntity<UserInfo> responseEntity = restTemplate.exchange( SSO_VALIDATION_URL, HttpMethod.GET, entity, UserInfo.class); if (responseEntity.getStatusCode() == HttpStatus.OK) { // SSO Session有效,創(chuàng)建本地Session UserInfo userInfo = responseEntity.getBody(); HttpSession session = httpRequest.getSession(true); session.setAttribute("USER_INFO", userInfo); chain.doFilter(request, response); } else { // SSO Session無效,重定向到登錄頁 String redirectUrl = SSO_SERVER_URL + "/login?redirect=" + URLEncoder.encode(httpRequest.getRequestURL().toString(), "UTF-8"); httpResponse.sendRedirect(redirectUrl); } } catch (Exception e) { // 驗證過程出錯,重定向到登錄頁 String redirectUrl = SSO_SERVER_URL + "/login?redirect=" + URLEncoder.encode(httpRequest.getRequestURL().toString(), "UTF-8"); httpResponse.sendRedirect(redirectUrl); } } }
優(yōu)缺點
優(yōu)點:
- 實現(xiàn)相對簡單,遵循傳統(tǒng)Web開發(fā)模式
- 服務(wù)端完全控制會話狀態(tài)和生命周期
- 客戶端無需存儲和管理復(fù)雜狀態(tài)
- 支持即時會話失效和撤銷
缺點:
- 受同源策略限制,僅適用于同一頂級域名下的應(yīng)用
- 依賴Cookie機制,在某些環(huán)境可能受限(如移動應(yīng)用)
- 存在CSRF風險
二、基于JWT的無狀態(tài)SSO方案
原理
JWT(JSON Web Token)是一種緊湊的、自包含的令牌格式,可以在不同應(yīng)用間安全地傳遞信息。使用JWT實現(xiàn)SSO時,認證服務(wù)器在用戶登錄后生成JWT令牌,其中包含用戶相關(guān)信息和簽名。由于JWT可以獨立驗證而無需查詢中央服務(wù)器,它非常適合構(gòu)建無狀態(tài)的SSO系統(tǒng)。
實現(xiàn)方案
- 創(chuàng)建JWT認證服務(wù)
首先添加依賴:
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency>
實現(xiàn)JWT工具類:
@Component public class JwtUtil { @Value("${jwt.secret}") private String secretKey; @Value("${jwt.expiration}") private long expirationTime; @PostConstruct protected void init() { secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes()); } public String generateToken(User user) { Map<String, Object> claims = new HashMap<>(); claims.put("id", user.getId()); claims.put("username", user.getUsername()); claims.put("email", user.getEmail()); claims.put("roles", user.getRoles()); return createToken(claims, user.getUsername()); } private String createToken(Map<String, Object> claims, String subject) { Date now = new Date(); Date expiryDate = new Date(now.getTime() + expirationTime); return Jwts.builder() .setClaims(claims) .setSubject(subject) .setIssuedAt(now) .setExpiration(expiryDate) .signWith(Keys.hmacShaKeyFor(secretKey.getBytes()), SignatureAlgorithm.HS512) .compact(); } public boolean validateToken(String token) { try { Jwts.parserBuilder() .setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes())) .build() .parseClaimsJws(token); return true; } catch (Exception e) { return false; } } public Claims extractAllClaims(String token) { return Jwts.parserBuilder() .setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes())) .build() .parseClaimsJws(token) .getBody(); } }
實現(xiàn)認證控制器:
@RestController @RequestMapping("/auth") public class AuthController { @Autowired private UserService userService; @Autowired private JwtUtil jwtUtil; @PostMapping("/login") public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) { // 驗證用戶憑證 User user = userService.authenticate( loginRequest.getUsername(), loginRequest.getPassword() ); if (user == null) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED) .body(new ErrorResponse("Invalid credentials")); } // 生成JWT String jwt = jwtUtil.generateToken(user); // 設(shè)置刷新令牌(可選) String refreshToken = UUID.randomUUID().toString(); userService.saveRefreshToken(user.getId(), refreshToken, 30); // 30天有效期 // 返回Token return ResponseEntity.ok(new JwtResponse(jwt, refreshToken)); } @PostMapping("/refresh") public ResponseEntity<?> refreshToken(@RequestBody RefreshTokenRequest request) { String refreshToken = request.getRefreshToken(); // 驗證刷新令牌 User user = userService.findUserByRefreshToken(refreshToken); if (user == null) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED) .body(new ErrorResponse("Invalid refresh token")); } // 生成新的JWT String newToken = jwtUtil.generateToken(user); return ResponseEntity.ok(new JwtResponse(newToken, refreshToken)); } @PostMapping("/validate") public ResponseEntity<?> validateToken(@RequestBody TokenValidationRequest request) { String token = request.getToken(); if (!jwtUtil.validateToken(token)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED) .body(new ErrorResponse("Invalid token")); } // 提取用戶信息 Claims claims = jwtUtil.extractAllClaims(token); Map<String, Object> userInfo = new HashMap<>(claims); return ResponseEntity.ok(userInfo); } @PostMapping("/logout") public ResponseEntity<?> logout(@RequestBody LogoutRequest request) { // 刪除刷新令牌 userService.removeRefreshToken(request.getRefreshToken()); // JWT本身無法撤銷,客戶端需要丟棄令牌 return ResponseEntity.ok(new SuccessResponse("Logout successful")); } }
- 客戶端應(yīng)用集成
JWT過濾器:
@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 = request.getHeader("Authorization"); String username = null; String jwt = null; // 從Authorization頭提取JWT if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { jwt = authorizationHeader.substring(7); try { // 驗證令牌并提取用戶信息 if (jwtUtil.validateToken(jwt)) { Claims claims = jwtUtil.extractAllClaims(jwt); username = claims.getSubject(); // 構(gòu)建認證對象 List<String> roles = (List<String>) claims.get("roles"); List<GrantedAuthority> authorities = roles.stream() .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); UserDetails userDetails = new User(username, "", authorities); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (Exception e) { // JWT驗證失敗,不設(shè)置認證信息 } } chain.doFilter(request, response); } }
安全配置:
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private JwtRequestFilter jwtRequestFilter; @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/api/public/**", "/auth/login", "/auth/refresh").permitAll() .anyRequest().authenticated() .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
- 前端處理JWT
// 在登錄成功后存儲JWT令牌 function handleLoginSuccess(response) { const { token, refreshToken } = response.data; localStorage.setItem('jwtToken', token); localStorage.setItem('refreshToken', refreshToken); // 設(shè)置默認Authorization頭 axios.defaults.headers.common['Authorization'] = `Bearer ${token}`; // 重定向到應(yīng)用頁面 window.location.href = '/dashboard'; } // 添加請求攔截器自動附加令牌 axios.interceptors.request.use(config => { const token = localStorage.getItem('jwtToken'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); // 添加響應(yīng)攔截器處理令牌過期 axios.interceptors.response.use( response => response, async error => { const originalRequest = error.config; // 如果是401錯誤且不是刷新令牌請求,嘗試刷新令牌 if (error.response.status === 401 && !originalRequest._retry) { originalRequest._retry = true; try { const refreshToken = localStorage.getItem('refreshToken'); const response = await axios.post('/auth/refresh', { refreshToken }); const { token } = response.data; localStorage.setItem('jwtToken', token); axios.defaults.headers.common['Authorization'] = `Bearer ${token}`; return axios(originalRequest); } catch (err) { // 刷新失敗,重定向到登錄頁 localStorage.removeItem('jwtToken'); localStorage.removeItem('refreshToken'); window.location.href = '/login'; return Promise.reject(err); } } return Promise.reject(error); } );
優(yōu)缺點
優(yōu)點:
- 完全無狀態(tài),服務(wù)器不需要存儲會話信息
- 跨域支持,適用于分布式系統(tǒng)和微服務(wù)
- 可擴展性好,JWT可包含豐富的用戶信息
- 不依賴Cookie,避免CSRF問題
- 適用于各種客戶端(Web、移動應(yīng)用、API)
缺點:
- 無法主動失效已頒發(fā)的令牌(除非使用黑名單機制)
- JWT可能較大,增加網(wǎng)絡(luò)傳輸負擔
- 令牌管理需要客戶端介入
- 刷新令牌機制較復(fù)雜
- 存在令牌被盜用的風險
三、基于OAuth 2.0/OpenID Connect的SSO方案
原理
OAuth 2.0是一個授權(quán)框架,而OpenID Connect(OIDC)是建立在OAuth 2.0之上的身份認證層。這是目前最標準化和完善的SSO解決方案,特別適合企業(yè)級應(yīng)用和需要第三方集成的場景。它提供了豐富的授權(quán)流程選項和安全特性。
實現(xiàn)方案
在SpringBoot中,可以使用Spring Security OAuth2來實現(xiàn)OAuth 2.0/OIDC服務(wù)器和客戶端。
- 搭建授權(quán)服務(wù)器
首先添加依賴:
<dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> <version>2.6.8</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-jose</artifactId> </dependency>
配置授權(quán)服務(wù)器:
@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(); converter.setSigningKey("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(); tokenEnhancerChain.setTokenEnhancers( Arrays.asList(tokenEnhancer(), accessTokenConverter())); endpoints .tokenStore(tokenStore()) .tokenEnhancer(tokenEnhancerChain) .authenticationManager(authenticationManager) .userDetailsService(userDetailsService) .reuseRefreshTokens(false); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.jdbc(dataSource) .withClient("web-client") .secret(passwordEncoder.encode("web-client-secret")) .authorizedGrantTypes("password", "refresh_token", "authorization_code") .scopes("read", "write") .redirectUris("http://app1.example.com/login/oauth2/code/custom", "http://app2.example.com/login/oauth2/code/custom") .accessTokenValiditySeconds(3600) .refreshTokenValiditySeconds(86400) .and() .withClient("mobile-client") .secret(passwordEncoder.encode("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(); } } // 自定義令牌增強器,添加額外的用戶信息 public class CustomTokenEnhancer implements TokenEnhancer { @Override public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { if (authentication.getPrincipal() instanceof UserDetails) { UserDetails userDetails = (UserDetails) authentication.getPrincipal(); Map<String, Object> additionalInfo = new HashMap<>(); // 添加額外的用戶信息 if (userDetails instanceof CustomUserDetails) { CustomUserDetails customUserDetails = (CustomUserDetails) userDetails; additionalInfo.put("userId", customUserDetails.getId()); additionalInfo.put("email", customUserDetails.getEmail()); additionalInfo.put("fullName", customUserDetails.getFullName()); } ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo); } return accessToken; } }
配置資源服務(wù)器:
@Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/api/public/**").permitAll() .antMatchers("/api/user/**").hasRole("USER") .antMatchers("/api/admin/**").hasRole("ADMIN") .anyRequest().authenticated(); } }
- 客戶端應(yīng)用集成
客戶端配置(application.yml):
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: http://auth-server.example.com/oauth/authorize token-uri: http://auth-server.example.com/oauth/token user-info-uri: http://auth-server.example.com/userinfo jwk-set-uri: http://auth-server.example.com/.well-known/jwks.json user-name-attribute: sub
客戶端安全配置:
@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("http://auth-server.example.com/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 = delegate.loadUser(userRequest); // 自定義用戶信息處理 Map<String, Object> attributes = oAuth2User.getAttributes(); String userNameAttributeName = userRequest.getClientRegistration() .getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); return new DefaultOAuth2User( oAuth2User.getAuthorities(), attributes, userNameAttributeName); }; } }
- 完整的登出流程
@Controller public class AuthController { @GetMapping("/logout") public String logout(HttpServletRequest request, HttpServletResponse response, @RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient) { // 清除本地會話 SecurityContextHolder.clearContext(); HttpSession session = request.getSession(false); if (session != null) { session.invalidate(); } // 清除Cookies Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { cookie.setValue(""); cookie.setPath("/"); cookie.setMaxAge(0); response.addCookie(cookie); } } // 重定向到OAuth2服務(wù)器的注銷端點 String logoutUrl = "http://auth-server.example.com/logout?client_id=" + authorizedClient.getClientRegistration().getClientId() + "&post_logout_redirect_uri=" + URLEncoder.encode("http://app.example.com/", StandardCharsets.UTF_8); return "redirect:" + logoutUrl; } }
優(yōu)缺點
優(yōu)點:
- 成熟的安全協(xié)議,廣泛采用的行業(yè)標準
- 支持多種認證流程(授權(quán)碼、隱式、密碼等)
- 令牌撤銷機制完善
- 可擴展性極好,適合企業(yè)級應(yīng)用
- 明確分離認證與授權(quán)職責
缺點:
- 實現(xiàn)復(fù)雜度高,小型應(yīng)用可能不合適
- 配置和理解學習曲線陡峭
四、基于Spring Session的共享會話SSO方案
原理
Spring Session提供了一個將會話數(shù)據(jù)存儲在共享外部存儲(如Redis)中的框架,使得不同的應(yīng)用之間能夠共享會話信息。這種方式特別適合基于Spring的同構(gòu)系統(tǒng),可以在保持簡單實現(xiàn)的同時解決分布式會話共享問題。
實現(xiàn)方案
- 添加依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency>
- 配置Spring Session
@Configuration @EnableRedisHttpSession public class SessionConfig { @Bean public LettuceConnectionFactory connectionFactory() { // Redis連接配置 RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration(); redisConfig.setHostName("redis-host"); redisConfig.setPort(6379); return new LettuceConnectionFactory(redisConfig); } @Bean public CookieSerializer cookieSerializer() { DefaultCookieSerializer serializer = new DefaultCookieSerializer(); serializer.setCookieName("SESSION"); // 使用統(tǒng)一的Cookie名 serializer.setCookiePath("/"); serializer.setDomainNamePattern("^.+?\.(\w+\.[a-z]+)$"); // 支持子域 serializer.setUseSecureCookie(true); serializer.setUseHttpOnlyCookie(true); return serializer; } @Bean public HttpSessionIdResolver httpSessionIdResolver() { return new CookieHttpSessionIdResolver(); } }
- 創(chuàng)建中央認證服務(wù)
@Controller @RequestMapping("/auth") public class CentralAuthController { @Autowired private UserService userService; @GetMapping("/login") public String loginPage(@RequestParam(required = false) String redirect, Model model) { model.addAttribute("redirectUrl", redirect); return "login"; } @PostMapping("/login") public String login(@RequestParam String username, @RequestParam String password, @RequestParam(required = false) String redirect, HttpSession session, RedirectAttributes redirectAttrs) { // 驗證用戶憑證 User user = userService.authenticate(username, password); if (user == null) { redirectAttrs.addFlashAttribute("error", "Invalid credentials"); return "redirect:/auth/login"; } // 將用戶信息存入共享會話 UserInfo userInfo = new UserInfo(user.getId(), user.getUsername(), user.getRoles()); session.setAttribute("USER_INFO", userInfo); // 如果有重定向URL,則重定向回原應(yīng)用 if (redirect != null && !redirect.isEmpty()) { return "redirect:" + redirect; } return "redirect:/dashboard"; } @GetMapping("/logout") public String logout(HttpServletRequest request, HttpSession session) { // 使會話失效 session.invalidate(); // 可選:獲取要重定向的URL String referer = request.getHeader("Referer"); return "redirect:/auth/login"; } }
- 創(chuàng)建應(yīng)用內(nèi)認證過濾器
@Component public class SessionAuthenticationFilter implements Filter { private static final String LOGIN_PAGE = "http://auth.example.com/auth/login"; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; // 公開路徑不需要認證 String path = httpRequest.getRequestURI(); if (isPublicPath(path)) { chain.doFilter(request, response); return; } // 檢查session中是否有用戶信息 HttpSession session = httpRequest.getSession(false); boolean authenticated = session != null && session.getAttribute("USER_INFO") != null; if (authenticated) { // 用戶已認證,繼續(xù)請求 chain.doFilter(request, response); } else { // 未認證,重定向到登錄頁面 String redirectUrl = LOGIN_PAGE + "?redirect=" + URLEncoder.encode(httpRequest.getRequestURL().toString(), "UTF-8"); httpResponse.sendRedirect(redirectUrl); } } private boolean isPublicPath(String path) { return path.startsWith("/public/") || path.startsWith("/resources/") || path.equals("/error"); } }
- 配置應(yīng)用內(nèi)WebMVC
@Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private SessionAuthenticationFilter sessionAuthenticationFilter; @Bean public FilterRegistrationBean<SessionAuthenticationFilter> sessionFilterRegistration() { FilterRegistrationBean<SessionAuthenticationFilter> registration = new FilterRegistrationBean<>(); registration.setFilter(sessionAuthenticationFilter); registration.addUrlPatterns("/*"); registration.setOrder(Ordered.HIGHEST_PRECEDENCE + 1); // 在Spring Security之后執(zhí)行 return registration; } }
- 使用會話信息
@Controller @RequestMapping("/dashboard") public class DashboardController { @GetMapping public String dashboard(HttpSession session, Model model) { UserInfo userInfo = (UserInfo) session.getAttribute("USER_INFO"); model.addAttribute("user", userInfo); return "dashboard"; } }
優(yōu)缺點
優(yōu)點:
- 實現(xiàn)簡單,易于理解
- 與Spring生態(tài)無縫集成
- 會話可包含豐富的信息
缺點:
- 依賴中央存儲(如Redis)
- 會話數(shù)據(jù)需要序列化/反序列化
- 依賴Cookie,不適合非Web應(yīng)用
五、方案選擇與最佳實踐
選擇建議
方案類型 | 推薦場景 | 不適合場景 |
---|---|---|
Cookie-Session | 同域名下的小型應(yīng)用,簡單的認證需求 | 跨域應(yīng)用,移動應(yīng)用集成,高安全性需求 |
JWT | 分布式微服務(wù),前后端分離應(yīng)用 | 需要即時撤銷令牌的場景,極高安全性要求 |
OAuth 2.0/OIDC | 企業(yè)級應(yīng)用,需要第三方集成,多租戶系統(tǒng) | 小型應(yīng)用,資源受限環(huán)境,急速開發(fā)需求 |
Spring Session | Spring技術(shù)棧的應(yīng)用,中型企業(yè)應(yīng)用 | 異構(gòu)技術(shù)棧,非Web應(yīng)用集成 |
六、總結(jié)
從簡單的基于Cookie-Session的方案,到復(fù)雜的OAuth 2.0/OIDC實現(xiàn),SSO方案的選擇應(yīng)該基于業(yè)務(wù)需求、安全要求、用戶體驗?zāi)繕撕图夹g(shù)約束進行綜合考量。
以上就是SpringBoot實現(xiàn)單點登錄(SSO)的四種方案的詳細內(nèi)容,更多關(guān)于SpringBoot單點登錄SSO的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring Boot 中的 Spring Cloud Feign的原
Spring Cloud Feign 是 Spring Cloud 中的一個組件,它可以幫助我們實現(xiàn)聲明式的 REST 客戶,這篇文章主要介紹了Spring Boot 中的 Spring Cloud Feign,需要的朋友可以參考下2023-07-07Java實現(xiàn)Excel導(dǎo)入導(dǎo)出數(shù)據(jù)庫的方法示例
這篇文章主要介紹了Java實現(xiàn)Excel導(dǎo)入導(dǎo)出數(shù)據(jù)庫的方法,結(jié)合實例形式分析了java針對Excel的讀寫及數(shù)據(jù)庫操作相關(guān)實現(xiàn)技巧,需要的朋友可以參考下2017-08-08四步輕松搞定java web每天定時執(zhí)行任務(wù)
本篇文章主要介紹了四步輕松搞定java web每天定時執(zhí)行任務(wù),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-01-01解決springboot報錯Could not resolve placeholder‘x
這篇文章主要介紹了解決springboot報錯:Could not resolve placeholder ‘xxx‘ in value “${XXXX}問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-11-11Spring Cache優(yōu)化數(shù)據(jù)庫訪問的項目實踐
本文主要介紹了Spring Cache優(yōu)化數(shù)據(jù)庫訪問的項目實踐,將創(chuàng)建一個簡單的圖書管理應(yīng)用作為示例,并演示如何通過緩存減少對數(shù)據(jù)庫的頻繁查詢,感興趣的可以了解一下2024-01-01