欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

SpringBoot實現(xiàn)單點登錄(SSO)的四種方案

 更新時間:2025年04月07日 08:51:31   作者:風象南  
單點登錄(Single?Sign-On,SSO)是企業(yè)應(yīng)用系統(tǒng)中常見的用戶認證方案,它允許用戶使用一組憑證訪問多個相關(guān)但獨立的系統(tǒng),無需重復(fù)登錄,本文給大家介紹了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 SessionSpring技術(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)文章

  • java實現(xiàn)文件下載的兩種方式

    java實現(xiàn)文件下載的兩種方式

    這篇文章主要為大家詳細介紹了java實現(xiàn)文件下載的兩種方式,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2017-11-11
  • Spring Boot 中的 Spring Cloud Feign的原理解析

    Spring Boot 中的 Spring Cloud Feign的原

    Spring Cloud Feign 是 Spring Cloud 中的一個組件,它可以幫助我們實現(xiàn)聲明式的 REST 客戶,這篇文章主要介紹了Spring Boot 中的 Spring Cloud Feign,需要的朋友可以參考下
    2023-07-07
  • Java實現(xiàn)Excel導(dǎo)入導(dǎo)出數(shù)據(jù)庫的方法示例

    Java實現(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ù)

    本篇文章主要介紹了四步輕松搞定java web每天定時執(zhí)行任務(wù),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-01-01
  • Java?導(dǎo)出Excel增加下拉框選項

    Java?導(dǎo)出Excel增加下拉框選項

    這篇文章主要介紹了Java?導(dǎo)出Excel增加下拉框選項,excel對于下拉框較多選項的,需要使用隱藏工作簿來解決,使用函數(shù)取值來做選項,下文具體的操作詳情,需要的小伙伴可以參考一下
    2022-04-04
  • Java實現(xiàn)遞歸刪除菜單和目錄及目錄下所有文件

    Java實現(xiàn)遞歸刪除菜單和目錄及目錄下所有文件

    這篇文章主要為大家詳細介紹了Java如何實現(xiàn)遞歸刪除菜單和刪除目錄及目錄下所有文件,文中的示例代碼講解詳細,感興趣的小伙伴可以參考一下
    2025-03-03
  • 深入探究HashMap二次Hash原因

    深入探究HashMap二次Hash原因

    在java開發(fā)中,HashMap是最常用、最常見的集合容器類之一,文中通過示例代碼介紹HashMap為啥要二次Hash,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2023-01-01
  • Java正則表達式匹配電話格式

    Java正則表達式匹配電話格式

    正則表達式是由普通的字符以及特殊字符組成的文字模式,用來在查找文字主體時待匹配的一個或多個字符串。本文給大家介紹java正則表達式匹配電話格式,對java正則表達式匹配相關(guān)知識感興趣的朋友一起學習吧
    2015-11-11
  • 解決springboot報錯Could not resolve placeholder‘xxx‘ in value“${XXXX}

    解決springboot報錯Could not resolve placeholder‘x

    這篇文章主要介紹了解決springboot報錯:Could not resolve placeholder ‘xxx‘ in value “${XXXX}問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2023-11-11
  • Spring Cache優(yōu)化數(shù)據(jù)庫訪問的項目實踐

    Spring Cache優(yōu)化數(shù)據(jù)庫訪問的項目實踐

    本文主要介紹了Spring Cache優(yōu)化數(shù)據(jù)庫訪問的項目實踐,將創(chuàng)建一個簡單的圖書管理應(yīng)用作為示例,并演示如何通過緩存減少對數(shù)據(jù)庫的頻繁查詢,感興趣的可以了解一下
    2024-01-01

最新評論