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.Cookie; 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.Enumeration; import java.util.HashMap; import java.util.List; /** * @author takensoft * @since 2025.05.22 * @modification * since | author | description * 2025.05.22 | takensoft | 최초 등록 * 2025.05.26 | takensoft | OAuth 리다이렉트 기능 추가 * 2025.05.28 | 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; 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"); /** * 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 { // 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; 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(); // 현재 로그인한 사용자 ID 조회 String currentUserId = null; MberVO mberInfo = null; if ("S".equals(loginMode)) { // 세션 모드 - 세션에서 직접 조회 HttpSession session = request.getSession(false); if (session != null) { currentUserId = (String) session.getAttribute("mbrId"); if (currentUserId != null) { // 세션에 저장된 정보로 응답 생성 HashMap result = new HashMap<>(); result.put("mbrId", session.getAttribute("mbrId")); result.put("mbrNm", session.getAttribute("mbrNm")); result.put("roles", session.getAttribute("roles")); return resUtil.successRes(result, MessageCode.COMMON_SUCCESS); } } else { return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); } } else { // JWT 모드 - 토큰에서 조회 String authHeader = request.getHeader("Authorization"); System.out.println("Authorization Header: " + authHeader); String token = null; if (authHeader != null && !authHeader.isEmpty()) { // Authorization 헤더에서 토큰 추출 token = jwtUtil.extractToken(authHeader); } else { // Authorization 헤더가 없으면 OAuth 전용 쿠키에서 확인 token = getTokenFromOAuthCookie(request); } if (token == null || token.isEmpty()) { return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); } try { currentUserId = (String) jwtUtil.getClaim(token, "mbrId"); // JWT 모드에서는 DB에서 최신 정보 조회 HashMap params = new HashMap<>(); params.put("mbrId", currentUserId); mberInfo = mberService.findByMbr(params); if (mberInfo == null) { return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); } // 응답 데이터 구성 HashMap result = new HashMap<>(); result.put("mbrId", mberInfo.getMbrId()); result.put("mbrNm", mberInfo.getMbrNm()); result.put("roles", mberInfo.getAuthorList()); // 토큰도 함께 전달 if (authHeader != null && !authHeader.isEmpty()) { result.put("token", authHeader); } else { // 쿠키에서 가져온 토큰이면 Bearer 형태로 반환 String oauthToken = getTokenFromOAuthCookie(request); if (oauthToken != null) { result.put("token", "Bearer " + oauthToken); } } return resUtil.successRes(result, MessageCode.COMMON_SUCCESS); } catch (IllegalArgumentException e) { return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); } } // 여기까지 왔다면 사용자를 찾을 수 없음 return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); } catch (Exception e) { return resUtil.errorRes(MessageCode.COMMON_UNKNOWN_ERROR); } } /** * OAuth 전용 쿠키에서 access token 추출 */ private String getTokenFromOAuthCookie(HttpServletRequest request) { if (request.getCookies() != null) { for (Cookie cookie : request.getCookies()) { if ("oauth_access_token".equals(cookie.getName())) { return cookie.getValue(); } } } 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 validateSecurity(HttpServletRequest request) { 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.page?error=%s&message=%s", FRONT_URL, errorCode, encodedMessage); response.sendRedirect(errorUrl); } }