package com.takensoft.common.oauth.handler; import com.takensoft.cms.loginPolicy.service.LoginModeService; import com.takensoft.cms.loginPolicy.service.LoginPolicyService; import com.takensoft.cms.mber.service.LgnHstryService; import com.takensoft.cms.mber.service.UnifiedLoginService; import com.takensoft.cms.mber.vo.LgnHstryVO; import com.takensoft.cms.mber.vo.MberVO; import com.takensoft.common.oauth.vo.CustomOAuth2UserVO; import com.takensoft.common.util.HttpRequestUtil; import com.takensoft.common.util.LoginUtil; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import org.springframework.stereotype.Component; import java.io.IOException; import java.net.URLEncoder; import java.util.Map; /** * @author takensoft * @since 2025.05.22 * @modification * since | author | description * 2025.05.22 | takensoft | 최초 등록 * 2025.05.28 | takensoft | 통합 로그인 적용 * 2025.05.29 | takensoft | OAuth2 통합 문제 해결 * 2025.06.02 | takensoft | 세션 모드 중복로그인 처리 개선 * 2025.06.09 | takensoft | OIDC 타입 캐스팅 문제 해결 * * OAuth2 로그인 성공 핸들러 */ @Slf4j @Component @RequiredArgsConstructor public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { private final UnifiedLoginService unifiedLoginService; private final LgnHstryService lgnHstryService; private final HttpRequestUtil httpRequestUtil; private final LoginUtil loginUtil; private final LoginModeService loginModeService; private final LoginPolicyService loginPolicyService; @Value("${front.url}") private String frontUrl; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { try { // OAuth2User 타입 확인 및 정보 추출 OAuth2UserInfo userInfo = extractUserInfo(authentication); if (userInfo == null) { handleOAuth2Error(response, new Exception("사용자 정보 추출 실패")); return; } // 현재 설정된 로그인 모드 확인 String currentLoginMode = loginModeService.getLoginMode(); boolean allowMultipleLogin = loginPolicyService.getPolicy(); // 통합 로그인 서비스를 통한 OAuth2 사용자 처리 MberVO mber = unifiedLoginService.processOAuth2User( userInfo.email, unifiedLoginService.convertProviderToMbrType(userInfo.provider), userInfo.id, userInfo.name, request ); // OAuth2 로그인 이력 저장 saveLoginHistory(request, mber, userInfo.provider); request.setAttribute("loginType", "OAUTH2"); // LoginUtil을 통한 통합 로그인 처리 loginUtil.successLogin(mber, request, response); String redirectUrl = String.format("%s/?oauth_success=true&loginMode=%s&policyMode=%s", frontUrl, currentLoginMode, allowMultipleLogin ? "Y" : "N"); getRedirectStrategy().sendRedirect(request, response, redirectUrl); } catch (Exception e) { handleOAuth2Error(response, e); } } /** * OAuth2User에서 사용자 정보 추출 (개선된 버전) */ private OAuth2UserInfo extractUserInfo(Authentication authentication) { Object principal = authentication.getPrincipal(); String provider = determineProvider(authentication); try { if (principal instanceof CustomOAuth2UserVO) { // 커스텀 OAuth2 사용자 CustomOAuth2UserVO customUser = (CustomOAuth2UserVO) principal; return new OAuth2UserInfo( customUser.getProvider(), customUser.getId(), customUser.getName(), customUser.getEmail() ); } else if (principal instanceof OidcUser) { // OIDC 사용자 (구글) OidcUser oidcUser = (OidcUser) principal; return extractOidcUserInfo(oidcUser, provider); } else if (principal instanceof OAuth2User) { // 일반 OAuth2 사용자 OAuth2User oauth2User = (OAuth2User) principal; return extractOAuth2UserInfo(oauth2User, provider); } else { return null; } } catch (Exception e) { return null; } } /** * OIDC 사용자 정보 추출 (구글) */ private OAuth2UserInfo extractOidcUserInfo(OidcUser oidcUser, String provider) { try { String id = oidcUser.getSubject(); // OIDC의 subject가 사용자 ID String name = oidcUser.getFullName(); if (name == null || name.trim().isEmpty()) { name = oidcUser.getGivenName(); } if (name == null || name.trim().isEmpty()) { name = oidcUser.getEmail(); } String email = oidcUser.getEmail(); return new OAuth2UserInfo(provider, id, name, email); } catch (Exception e) { return null; } } /** * 일반 OAuth2 사용자 정보 추출 */ private OAuth2UserInfo extractOAuth2UserInfo(OAuth2User oauth2User, String provider) { try { Map attributes = oauth2User.getAttributes(); String id = null; String name = null; String email = null; // 제공자별 정보 추출 switch (provider.toLowerCase()) { case "kakao": id = String.valueOf(attributes.get("id")); Map kakaoAccount = (Map) attributes.get("kakao_account"); if (kakaoAccount != null) { email = (String) kakaoAccount.get("email"); Map profile = (Map) kakaoAccount.get("profile"); if (profile != null) { name = (String) profile.get("nickname"); } } break; case "naver": Map naverResponse = (Map) attributes.get("response"); if (naverResponse != null) { id = (String) naverResponse.get("id"); name = (String) naverResponse.get("name"); email = (String) naverResponse.get("email"); } break; case "google": id = (String) attributes.get("sub"); if (id == null) id = (String) attributes.get("id"); name = (String) attributes.get("name"); email = (String) attributes.get("email"); break; } return new OAuth2UserInfo(provider, id, name, email); } catch (Exception e) { return null; } } /** * 제공자 결정 */ private String determineProvider(Authentication authentication) { if (authentication instanceof OAuth2AuthenticationToken) { OAuth2AuthenticationToken oauth2Token = (OAuth2AuthenticationToken) authentication; String registrationId = oauth2Token.getAuthorizedClientRegistrationId(); return registrationId.toLowerCase(); // 소문자로 통일 } String name = authentication.getName(); if (name != null) { String lowerName = name.toLowerCase(); if (lowerName.contains("google")) return "google"; if (lowerName.contains("kakao")) return "kakao"; if (lowerName.contains("naver")) return "naver"; } // 기본값 return "google"; } /** * 로그인 이력 저장 - OAuth2 전용 */ private void saveLoginHistory(HttpServletRequest request, MberVO mber, String provider) { try { String userAgent = httpRequestUtil.getUserAgent(request); LgnHstryVO loginHistory = new LgnHstryVO(); loginHistory.setLgnId(mber.getLgnId()); loginHistory.setLgnType(mber.getAuthorities().stream().anyMatch(r -> r.getAuthority().equals("ROLE_ADMIN")) ? "0" : "1"); loginHistory.setCntnIp(httpRequestUtil.getIp(request)); loginHistory.setCntnOperSysm(httpRequestUtil.getOS(userAgent)); loginHistory.setDvcNm(httpRequestUtil.getDevice(userAgent)); loginHistory.setBrwsrNm(httpRequestUtil.getBrowser(userAgent)); lgnHstryService.LgnHstrySave(loginHistory); } catch (Exception e) { log.error("로그인 이력 저장 실패", e); } } /** * OAuth2 오류 처리 */ private void handleOAuth2Error(HttpServletResponse response, Exception e) throws IOException { String message = URLEncoder.encode("OAuth 로그인에 실패했습니다.", "UTF-8"); String errorUrl = String.format("%s/?error=oauth2_failed&message=%s", frontUrl, message); getRedirectStrategy().sendRedirect(null, response, errorUrl); } /** * OAuth2 사용자 정보 내부 클래스 */ private static class OAuth2UserInfo { final String provider; final String id; final String name; final String email; OAuth2UserInfo(String provider, String id, String name, String email) { this.provider = provider; this.id = id; this.name = name; this.email = email; } } }