package com.takensoft.common.oauth.web; import com.takensoft.cms.loginPolicy.service.LoginModeService; 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.JWTUtil; 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.*; 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 리다이렉트 기능 추가 * 2025.05.28 | takensoft | 쿠키에서 OAuth 토큰 읽기 추가 * 2025.06.18 | takensoft | 토큰 추출 로직 통합 및 중복 제거 * 2025.06.18 | takensoft | 사용자 정보 응답 생성 로직 통합 완료 * * 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; private final LoginModeService loginModeService; private final JWTUtil jwtUtil; @Value("${front.url}") private String FRONT_URL; // 지원하는 OAuth 제공자 목록 private static final List SUPPORTED_PROVIDERS = Arrays.asList("kakao", "naver", "google"); // 응답 키 상수 정의 private static final String RESPONSE_KEY_MBR_ID = "mbrId"; private static final String RESPONSE_KEY_MBR_NM = "mbrNm"; private static final String RESPONSE_KEY_ROLES = "roles"; private static final String RESPONSE_KEY_LOGIN_MODE = "loginMode"; private static final String RESPONSE_KEY_TOKEN = "token"; private static final String RESPONSE_KEY_EMAIL = "email"; /** * 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); try { // Provider 유효성 검증 validateProvider(provider); // 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", "*"); // OAuth2 Authorization Server로 리다이렉트 String redirectUrl = "/oauth2/authorization/" + provider; response.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY); response.sendRedirect(redirectUrl); } catch (IllegalArgumentException e) { handleError(response, "invalid_provider", e.getMessage()); } catch (SecurityException e) { handleError(response, "security_check_failed", e.getMessage()); } catch (Exception e) { handleError(response, "system_error", "OAuth 로그인 중 오류가 발생했습니다."); } } /** * OAuth2 로그인 후 사용자 정보 조회 */ @PostMapping(value = "/getUserInfo.json") public ResponseEntity getUserInfo(HttpServletRequest request) { try { // 로그인 모드 확인 String loginMode = loginModeService.getLoginMode(); if ("S".equals(loginMode)) { // 세션 모드 처리 return handleSessionModeUserInfo(request); } else { // JWT 모드 처리 return handleJWTModeUserInfo(request); } } catch (Exception e) { log.error("사용자 정보 조회 중 오류 발생", e); return resUtil.errorRes(MessageCode.COMMON_UNKNOWN_ERROR); } } /** * 세션 모드 사용자 정보 처리 */ private ResponseEntity handleSessionModeUserInfo(HttpServletRequest request) { try { HttpSession session = request.getSession(false); if (session == null) { return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); } String currentUserId = (String) session.getAttribute("mbrId"); if (currentUserId == null) { return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); } // 사용자 정보 조회 MberVO mberInfo = getUserById(currentUserId); if (mberInfo == null) { return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); } // 통합된 응답 생성 Map result = createUserInfoResponse(mberInfo, "S", null, null); return resUtil.successRes(result, MessageCode.COMMON_SUCCESS); } catch (Exception e) { log.error("세션 모드 사용자 정보 처리 중 오류", e); return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); } } /** * JWT 모드 사용자 정보 처리 */ private ResponseEntity handleJWTModeUserInfo(HttpServletRequest request) { try { // 통합된 토큰 추출 로직 사용 String token = jwtUtil.extractTokenFromRequest(request); if (token == null) { return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); } // 토큰에서 사용자 ID 추출 String currentUserId = extractUserIdFromToken(token); if (currentUserId == null) { return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); } // 사용자 정보 조회 MberVO mberInfo = getUserById(currentUserId); if (mberInfo == null) { return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); } // 통합된 응답 생성 (토큰 포함) Map result = createUserInfoResponse(mberInfo, "J", "Bearer " + token, null); return resUtil.successRes(result, MessageCode.COMMON_SUCCESS); } catch (Exception e) { log.error("JWT 모드 사용자 정보 처리 중 오류", e); return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); } } /** * @param mberInfo - 사용자 정보 * @param loginMode - 로그인 모드 ("J", "S") * @param token - JWT 토큰 (JWT 모드일 때만) * @param additionalData - 추가 데이터 (필요시) * @return Map - 표준화된 사용자 정보 응답 * * 통합된 사용자 정보 응답 생성 메서드 - 모든 곳에서 사용 가능 */ public Map createUserInfoResponse(MberVO mberInfo, String loginMode, String token, Map additionalData) { if (mberInfo == null) { throw new IllegalArgumentException("사용자 정보가 null입니다."); } Map result = new HashMap<>(); // 기본 사용자 정보 설정 result.put(RESPONSE_KEY_MBR_ID, mberInfo.getMbrId()); result.put(RESPONSE_KEY_MBR_NM, mberInfo.getMbrNm()); result.put(RESPONSE_KEY_ROLES, mberInfo.getAuthorList()); result.put(RESPONSE_KEY_LOGIN_MODE, loginMode); // 이메일 정보 (있는 경우) if (mberInfo.getEml() != null && !mberInfo.getEml().trim().isEmpty()) { result.put(RESPONSE_KEY_EMAIL, mberInfo.getEml()); } // JWT 모드인 경우 토큰 추가 if ("J".equals(loginMode) && token != null && !token.trim().isEmpty()) { result.put(RESPONSE_KEY_TOKEN, token); } // 추가 데이터가 있으면 병합 if (additionalData != null && !additionalData.isEmpty()) { result.putAll(additionalData); } log.debug("사용자 정보 응답 생성 완료 - 사용자: {}, 모드: {}, 토큰포함: {}", mberInfo.getMbrId(), loginMode, token != null); return result; } /** * @param mberInfo - 사용자 정보 * @param loginMode - 로그인 모드 * @return Map - 기본 사용자 정보 응답 (토큰 없음) * * 기본 사용자 정보 응답 생성 */ public Map createBasicUserInfoResponse(MberVO mberInfo, String loginMode) { return createUserInfoResponse(mberInfo, loginMode, null, null); } /** * @param mberInfo - 사용자 정보 * @param token - JWT 토큰 * @return Map - JWT 사용자 정보 응답 * * JWT 사용자 정보 응답 생성 */ public Map createJWTUserInfoResponse(MberVO mberInfo, String token) { return createUserInfoResponse(mberInfo, "J", token, null); } /** * @param mberInfo - 사용자 정보 * @return Map - 세션 사용자 정보 응답 * * 세션 사용자 정보 응답 생성 */ public Map createSessionUserInfoResponse(MberVO mberInfo) { return createUserInfoResponse(mberInfo, "S", null, null); } /** * 사용자 ID로 사용자 정보 조회 */ private MberVO getUserById(String userId) { try { HashMap params = new HashMap<>(); params.put("mbrId", userId); return mberService.findByMbr(params); } catch (Exception e) { log.error("사용자 정보 조회 실패 - 사용자 ID: {}", userId, e); return null; } } /** * 토큰에서 사용자 ID 추출 */ private String extractUserIdFromToken(String token) { try { String currentUserId = (String) jwtUtil.getClaim("Bearer " + token, "mbrId"); if (currentUserId == null || currentUserId.isEmpty()) { log.warn("토큰에서 사용자 ID를 찾을 수 없습니다."); return null; } return currentUserId; } catch (Exception e) { log.error("토큰에서 사용자 ID 추출 실패", e); return null; } } /** * 지원하는 OAuth 제공자 목록 조회 API */ @GetMapping("/providers") public ResponseEntity getSupportedProviders() { 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); } } /** * 에러 발생 시 프론트엔드로 리다이렉트 */ 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.page?error=%s&message=%s", FRONT_URL, errorCode, encodedMessage); response.sendRedirect(errorUrl); } }