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.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 { // 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) { 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); } // 세션에서 정보 조회 및 최신 권한 정보 가져오기 HashMap params = new HashMap<>(); params.put("mbrId", currentUserId); MberVO 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()); result.put("loginMode", "S"); return resUtil.successRes(result, MessageCode.COMMON_SUCCESS); } catch (Exception e) { return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); } } /** * JWT 모드 사용자 정보 처리 */ private ResponseEntity handleJWTModeUserInfo(HttpServletRequest request) { try { String token = extractToken(request); if (token == null) { return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); } // 토큰에서 사용자 ID 추출 String currentUserId; try { currentUserId = (String) jwtUtil.getClaim(token, "mbrId"); if (currentUserId == null || currentUserId.isEmpty()) { return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); } } catch (Exception e) { return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); } // DB에서 최신 사용자 정보 조회 HashMap params = new HashMap<>(); params.put("mbrId", currentUserId); MberVO 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()); result.put("loginMode", "J"); // JWT 토큰도 함께 전달 String authHeader = request.getHeader("Authorization"); if (authHeader != null && !authHeader.isEmpty()) { result.put("token", authHeader); } else { // 쿠키에서 가져온 토큰이면 Bearer 형태로 반환 result.put("token", "Bearer " + token); } return resUtil.successRes(result, MessageCode.COMMON_SUCCESS); } catch (Exception e) { return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); } } /** * 토큰 추출 로직 통합 및 개선 */ private String extractToken(HttpServletRequest request) { // Authorization 헤더에서 토큰 추출 시도 String authHeader = request.getHeader("Authorization"); if (authHeader != null && !authHeader.isEmpty()) { return jwtUtil.extractToken(authHeader); } // OAuth 전용 쿠키에서 토큰 추출 시도 String oauthToken = getTokenFromOAuthCookie(request); if (oauthToken != null && !oauthToken.isEmpty()) { return oauthToken; } // 일반 Authorization 쿠키에서 토큰 추출 시도 if (request.getCookies() != null) { for (Cookie cookie : request.getCookies()) { if ("Authorization".equals(cookie.getName())) { return jwtUtil.extractToken(cookie.getValue()); } } } return null; } /** * OAuth 전용 쿠키에서 access token 추출 */ private String getTokenFromOAuthCookie(HttpServletRequest request) { if (request.getCookies() != null) { for (Cookie cookie : request.getCookies()) { if ("Authorization".equals(cookie.getName()) || "refresh".equals(cookie.getName())) { String token = cookie.getValue(); return token.startsWith("Bearer ") ? token.substring(7) : token; } } } 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); } }