
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
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<String> 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<String, Object> 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<String, Object> 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<String, Object> - 표준화된 사용자 정보 응답
*
* 통합된 사용자 정보 응답 생성 메서드 - 모든 곳에서 사용 가능
*/
public Map<String, Object> createUserInfoResponse(MberVO mberInfo, String loginMode, String token, Map<String, Object> additionalData) {
if (mberInfo == null) {
throw new IllegalArgumentException("사용자 정보가 null입니다.");
}
Map<String, Object> 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<String, Object> - 기본 사용자 정보 응답 (토큰 없음)
*
* 기본 사용자 정보 응답 생성
*/
public Map<String, Object> createBasicUserInfoResponse(MberVO mberInfo, String loginMode) {
return createUserInfoResponse(mberInfo, loginMode, null, null);
}
/**
* @param mberInfo - 사용자 정보
* @param token - JWT 토큰
* @return Map<String, Object> - JWT 사용자 정보 응답
*
* JWT 사용자 정보 응답 생성
*/
public Map<String, Object> createJWTUserInfoResponse(MberVO mberInfo, String token) {
return createUserInfoResponse(mberInfo, "J", token, null);
}
/**
* @param mberInfo - 사용자 정보
* @return Map<String, Object> - 세션 사용자 정보 응답
*
* 세션 사용자 정보 응답 생성
*/
public Map<String, Object> createSessionUserInfoResponse(MberVO mberInfo) {
return createUserInfoResponse(mberInfo, "S", null, null);
}
/**
* 사용자 ID로 사용자 정보 조회
*/
private MberVO getUserById(String userId) {
try {
HashMap<String, Object> 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);
}
}