package com.takensoft.common.oauth.web; import com.takensoft.cms.mber.service.MberService; import com.takensoft.cms.mber.vo.MberVO; import com.takensoft.common.message.MessageCode; import com.takensoft.common.service.VerificationService; import com.takensoft.common.util.HttpRequestUtil; import com.takensoft.common.util.ResponseUtil; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.io.IOException; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; /** * @author takensoft * @since 2025.05.22 * @modification * since | author | description * 2025.05.22 | takensoft | 최초 등록 * 2025.05.26 | takensoft | OAuth 리다이렉트 기능 추가 * * OAuth2 관련 통합 컨트롤러 */ @RestController @RequiredArgsConstructor @Slf4j @RequestMapping(value = "/oauth2") public class OAuth2Controller { private final MberService mberService; private final VerificationService verificationService; private final ResponseUtil resUtil; private final HttpRequestUtil httpRequestUtil; @Value("${front.url}") private String FRONT_URL; // 지원하는 OAuth 제공자 목록 private static final List SUPPORTED_PROVIDERS = Arrays.asList("kakao", "naver", "google"); /** * OAuth 로그인 리다이렉트 처리 * 프론트엔드에서 provider 정보를 받아 검증 후 OAuth2 서버로 리다이렉트 */ @GetMapping("/login") public void redirectToOAuth(@RequestParam String provider, HttpServletRequest request, HttpServletResponse response) throws IOException { String clientIP = httpRequestUtil.getIp(request); String userAgent = httpRequestUtil.getUserAgent(request); log.info("=== OAuth 로그인 시도 ==="); log.info("Provider: {}", provider); log.info("Client IP: {}", clientIP); log.info("User Agent: {}", userAgent); try { // 1. Provider 유효성 검증 validateProvider(provider); // 2. 보안 검증 (필요시 추가) validateSecurity(request); // 3. CORS 헤더 설정 (브라우저 보안 문제 해결) response.setHeader("Access-Control-Allow-Origin", FRONT_URL); response.setHeader("Access-Control-Allow-Credentials", "true"); response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); response.setHeader("Access-Control-Allow-Headers", "*"); // 4. OAuth2 Authorization Server로 리다이렉트 String redirectUrl = "/oauth2/authorization/" + provider; log.info("OAuth 리다이렉트 URL: {}", redirectUrl); // 리다이렉트 상태 코드를 명시적으로 설정 response.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY); response.sendRedirect(redirectUrl); } catch (IllegalArgumentException e) { log.error("OAuth 로그인 실패 - 잘못된 Provider: {}", provider); handleError(response, "invalid_provider", e.getMessage()); } catch (SecurityException e) { log.error("OAuth 로그인 실패 - 보안 검증 실패: {}", e.getMessage()); handleError(response, "security_check_failed", e.getMessage()); } catch (Exception e) { log.error("OAuth 로그인 실패 - 시스템 오류", e); handleError(response, "system_error", "OAuth 로그인 중 오류가 발생했습니다."); } } /** * OAuth2 로그인 후 사용자 정보 조회 */ @GetMapping(value = "/user-info") public ResponseEntity getUserInfo(HttpServletRequest request) { try { // 현재 로그인된 사용자 ID 추출 String currentUserId = verificationService.getCurrentUserId(); if (currentUserId == null || currentUserId.isEmpty()) { // 세션에서 OAuth2 정보 확인 (세션 모드인 경우) HttpSession session = request.getSession(false); if (session != null && session.getAttribute("oauth2User") != null) { return handleSessionOAuth2User(session); } return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); } // DB에서 사용자 정보 조회 HashMap params = new HashMap<>(); params.put("mbrId", currentUserId); MberVO userInfo = mberService.findByMbr(params); if (userInfo == null) { return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); } // 응답 데이터 구성 HashMap response = createUserResponse(userInfo); return resUtil.successRes(response, MessageCode.COMMON_SUCCESS); } catch (Exception e) { log.error("사용자 정보 조회 실패", e); return resUtil.errorRes(MessageCode.COMMON_UNKNOWN_ERROR); } } /** * 지원하는 OAuth 제공자 목록 조회 API */ @GetMapping("/providers") public ResponseEntity getSupportedProviders() { log.info("지원하는 OAuth 제공자 목록 조회"); return resUtil.successRes(SUPPORTED_PROVIDERS, MessageCode.COMMON_SUCCESS); } /** * OAuth 제공자 유효성 검증 */ private void validateProvider(String provider) { if (provider == null || provider.trim().isEmpty()) { throw new IllegalArgumentException("OAuth 제공자가 지정되지 않았습니다."); } if (!SUPPORTED_PROVIDERS.contains(provider.toLowerCase())) { throw new IllegalArgumentException("지원하지 않는 OAuth 제공자입니다: " + provider); } // TODO: 동적 설정으로 특정 제공자 비활성화 체크 // if (!oauthConfigService.isProviderEnabled(provider)) { // throw new IllegalArgumentException(provider + " 로그인이 일시 중단되었습니다."); // } } /** * 보안 검증 (필요시 확장) */ private void validateSecurity(HttpServletRequest request) { // TODO: 필요시 보안 검증 로직 추가 // 예: IP 화이트리스트, Rate Limiting, 사용자 상태 체크 등 String clientIP = httpRequestUtil.getIp(request); // 예시: 로컬 개발 환경이 아닌 경우 추가 검증 if (!"127.0.0.1".equals(clientIP) && !"::1".equals(clientIP)) { // 운영 환경 보안 검증 로직 } } /** * 에러 발생 시 프론트엔드로 리다이렉트 */ private void handleError(HttpServletResponse response, String errorCode, String errorMessage) throws IOException { String encodedMessage = java.net.URLEncoder.encode(errorMessage, "UTF-8"); String errorUrl = String.format("%s/login?error=%s&message=%s", FRONT_URL, errorCode, encodedMessage); log.info("에러 리다이렉트 URL: {}", errorUrl); response.sendRedirect(errorUrl); } /** * 세션의 OAuth2 사용자 정보 처리 */ private ResponseEntity handleSessionOAuth2User(HttpSession session) { try { // 세션에서 OAuth2 사용자 정보 추출 // 이는 DB 저장이 완료되기 전의 임시 상태 HashMap tempResponse = new HashMap<>(); tempResponse.put("mbrId", "TEMP_OAUTH2"); tempResponse.put("mbrNm", "OAuth2 User"); tempResponse.put("roles", new String[]{"ROLE_USER"}); tempResponse.put("isTemporary", true); return resUtil.successRes(tempResponse, MessageCode.COMMON_SUCCESS); } catch (Exception e) { log.error("세션 OAuth2 사용자 정보 처리 실패", e); return resUtil.errorRes(MessageCode.COMMON_UNKNOWN_ERROR); } } /** * 사용자 응답 데이터 생성 */ private HashMap createUserResponse(MberVO userInfo) { HashMap response = new HashMap<>(); response.put("mbrId", userInfo.getMbrId()); response.put("mbrNm", userInfo.getMbrNm()); response.put("eml", userInfo.getEml()); response.put("ncnm", userInfo.getNcnm()); response.put("mbrType", userInfo.getMbrType()); // 권한 정보 변환 String[] roles = userInfo.getAuthorList().stream() .map(auth -> auth.getAuthrtCd()) .toArray(String[]::new); response.put("roles", roles); response.put("isTemporary", false); return response; } }