
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.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<String> 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<String, Object> params = new HashMap<>();
params.put("mbrId", currentUserId);
MberVO mberInfo = mberService.findByMbr(params);
if (mberInfo == null) {
return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND);
}
// 응답 데이터 구성
HashMap<String, Object> 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<String, Object> params = new HashMap<>();
params.put("mbrId", currentUserId);
MberVO mberInfo = mberService.findByMbr(params);
if (mberInfo == null) {
return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND);
}
// 응답 데이터 구성
HashMap<String, Object> 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);
}
}