package com.arms.config.handler; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.server.WebFilterExchange; import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.util.UriComponentsBuilder; import reactor.core.publisher.Mono; @Component @Slf4j public class KeycloakLogoutHandler implements ServerLogoutHandler { private final WebClient webClient; private final String serverUrl; public KeycloakLogoutHandler(WebClient webClient, @Value("${spring.security.oauth2.client.registration.middle-proxy.server-url}")String serverUrl) { this.webClient = webClient; this.serverUrl = serverUrl; } @Override public Mono logout(WebFilterExchange exchange, Authentication authentication) { if (authentication == null) { log.warn("Logout called with null authentication, skipping Keycloak logout"); return Mono.empty(); } Object principalObj = authentication.getPrincipal(); if (!(principalObj instanceof OidcUser)) { log.warn("Principal is not OidcUser (actual type: {}), skipping Keycloak logout", principalObj != null ? principalObj.getClass().getName() : "null"); return Mono.empty(); } OidcUser principal = (OidcUser) principalObj; return logoutFromKeycloak(principal.getIdToken().getTokenValue()); } public Mono logoutFromKeycloak(String idToken) { String endSessionEndpoint = "http://keycloak:8080/auth" + "/realms/master/protocol/openid-connect/logout"; UriComponentsBuilder builder = UriComponentsBuilder .fromUriString(endSessionEndpoint) .queryParam("id_token_hint", idToken); return webClient.get() .uri(builder.toUriString()) .exchangeToMono(response -> { if (response.statusCode().is2xxSuccessful()) { log.info("Successfully logged out from Keycloak"); return Mono.empty(); } else { log.error("Could not propagate logout to Keycloak, status: {}", response.statusCode()); return Mono.empty(); // 로컬 세션은 정상 종료 처리 } }) .onErrorResume(ex -> { log.warn("Keycloak logout request failed ({}), proceeding with local session cleanup: {}", ex.getClass().getSimpleName(), ex.getMessage()); return Mono.empty(); // Connection Refused 등 네트워크 오류 시에도 로컬 로그아웃은 정상 처리 }); } }