diff --git a/src/main/java/com/grabpt/config/jwt/properties/CookieManager.java b/src/main/java/com/grabpt/config/jwt/properties/CookieManager.java deleted file mode 100644 index 29ca2071..00000000 --- a/src/main/java/com/grabpt/config/jwt/properties/CookieManager.java +++ /dev/null @@ -1,257 +0,0 @@ -package com.grabpt.config.jwt.properties; - -import static com.grabpt.config.jwt.properties.CookieConstants.*; - -import java.time.Duration; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.ResponseCookie; - -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.extern.slf4j.Slf4j; - -/** - * 통합 쿠키 관리 클래스 - * - 환경별(로컬/운영) 쿠키 설정 자동화 - * - 일관된 쿠키 생성/조회/삭제 인터페이스 - */ -@Slf4j -public final class CookieManager { - - private static final String PRODUCTION_DOMAIN = "grabpt.com"; - - private CookieManager() { - throw new AssertionError("Cannot instantiate utility class"); - } - - /** - * 환경 프로필 결정 - */ - private static class Environment { - final boolean isLocal; - final String domain; - final String sameSite; - final boolean secure; - - Environment(HttpServletRequest req) { - String host = getHeader(req, "X-Forwarded-Host", req.getServerName()); - this.isLocal = host != null && (host.contains("localhost") || host.contains("127.0.0.1")); - - if (isLocal) { - this.domain = null; // host-only - this.sameSite = "Lax"; - this.secure = false; - } else { - this.domain = PRODUCTION_DOMAIN; - this.sameSite = "None"; - this.secure = true; - } - } - - private String getHeader(HttpServletRequest req, String name, String fallback) { - String value = req.getHeader(name); - return value != null ? value : fallback; - } - } - - // ==================== JWT 토큰 쿠키 ==================== - - /** - * Access Token 쿠키 생성 (15분) - */ - public static void setAccessToken(HttpServletResponse response, HttpServletRequest request, String token) { - ResponseCookie cookie = createCookie(request, ACCESS_TOKEN, token) - .httpOnly(true) - .maxAge(Duration.ofMinutes(15)) - .path("/") - .build(); - addCookie(response, cookie); - } - - /** - * Refresh Token 쿠키 생성 (7일) - */ - public static void setRefreshToken(HttpServletResponse response, HttpServletRequest request, String token) { - Environment env = new Environment(request); - - ResponseCookie cookie = createCookie(request, REFRESH_TOKEN, token) - .httpOnly(true) - .maxAge(Duration.ofDays(7)) - .path(env.isLocal ? "/" : "/api/auth/reissue") // 운영: 특정 경로만 - .build(); - addCookie(response, cookie); - } - - /** - * Access Token 조회 - */ - public static String getAccessToken(HttpServletRequest request) { - return getCookieValue(request, ACCESS_TOKEN); - } - - /** - * Refresh Token 조회 - */ - public static String getRefreshToken(HttpServletRequest request) { - return getCookieValue(request, REFRESH_TOKEN); - } - - // ==================== 사용자 정보 쿠키 (공개) ==================== - - /** - * Role 쿠키 설정 (프론트에서 읽을 수 있음) - */ - public static void setRole(HttpServletResponse response, HttpServletRequest request, String role) { - ResponseCookie cookie = createCookie(request, ROLE, role) - .httpOnly(false) // 프론트에서 읽음 - .maxAge(Duration.ofDays(30)) - .path("/") - .build(); - addCookie(response, cookie); - } - - /** - * User ID 쿠키 설정 (프론트에서 읽을 수 있음) - */ - public static void setUserId(HttpServletResponse response, HttpServletRequest request, String userId) { - ResponseCookie cookie = createCookie(request, USER_ID, userId) - .httpOnly(false) - .maxAge(Duration.ofDays(30)) - .path("/") - .build(); - addCookie(response, cookie); - } - - // ==================== OAuth 임시 정보 쿠키 ==================== - - /** - * OAuth 회원가입용 임시 정보 설정 (5분) - * HttpOnly로 설정하여 XSS 방지 - */ - public static void setOAuthTempInfo(HttpServletResponse response, HttpServletRequest request, - String email, String name, String oauthId, String provider) { - Duration ttl = Duration.ofMinutes(5); - - addCookie(response, createCookie(request, OAUTH_EMAIL, email) - .httpOnly(true) // ⚠️ XSS 방지를 위해 HttpOnly 설정 - .maxAge(ttl).path("/").build()); - - addCookie(response, createCookie(request, OAUTH_NAME, name) - .httpOnly(true) - .maxAge(ttl).path("/").build()); - - addCookie(response, createCookie(request, OAUTH_ID, oauthId) - .httpOnly(true) - .maxAge(ttl).path("/").build()); - - addCookie(response, createCookie(request, OAUTH_PROVIDER, provider) - .httpOnly(true) - .maxAge(ttl).path("/").build()); - } - - /** - * OAuth 임시 정보 조회 (서버 사이드에서만) - */ - public static OAuthTempInfo getOAuthTempInfo(HttpServletRequest request) { - return new OAuthTempInfo( - getCookieValue(request, OAUTH_EMAIL), - getCookieValue(request, OAUTH_NAME), - getCookieValue(request, OAUTH_ID), - getCookieValue(request, OAUTH_PROVIDER) - ); - } - - public record OAuthTempInfo(String email, String name, String oauthId, String provider) { - public boolean isValid() { - return email != null && oauthId != null && provider != null; - } - } - - // ==================== 쿠키 삭제 ==================== - - /** - * 로그아웃 시 모든 인증 쿠키 삭제 - */ - public static void clearAuthCookies(HttpServletResponse response, HttpServletRequest request) { - deleteCookie(response, request, ACCESS_TOKEN, "/"); - deleteCookie(response, request, REFRESH_TOKEN, "/"); - deleteCookie(response, request, REFRESH_TOKEN, "/api/auth/reissue"); - deleteCookie(response, request, ROLE, "/"); - deleteCookie(response, request, USER_ID, "/"); - } - - /** - * OAuth 임시 정보 쿠키 삭제 - */ - public static void clearOAuthTempCookies(HttpServletResponse response, HttpServletRequest request) { - deleteCookie(response, request, OAUTH_EMAIL, "/"); - deleteCookie(response, request, OAUTH_NAME, "/"); - deleteCookie(response, request, OAUTH_ID, "/"); - deleteCookie(response, request, OAUTH_PROVIDER, "/"); - } - - // ==================== Helper Methods ==================== - - /** - * 쿠키 빌더 생성 (환경별 설정 자동 적용) - */ - private static ResponseCookie.ResponseCookieBuilder createCookie( - HttpServletRequest request, String name, String value) { - Environment env = new Environment(request); - - ResponseCookie.ResponseCookieBuilder builder = ResponseCookie - .from(name, value != null ? value : "") - .secure(env.secure) - .sameSite(env.sameSite); - - if (env.domain != null) { - builder.domain(env.domain); - } - - return builder; - } - - /** - * 쿠키 값 조회 - */ - private static String getCookieValue(HttpServletRequest request, String name) { - Cookie[] cookies = request.getCookies(); - if (cookies == null) { - return null; - } - - for (Cookie cookie : cookies) { - if (name.equals(cookie.getName())) { - String value = cookie.getValue(); - if (value != null && !value.isBlank()) { - return value; - } - } - } - return null; - } - - /** - * 쿠키 삭제 - */ - private static void deleteCookie(HttpServletResponse response, HttpServletRequest request, - String name, String path) { - ResponseCookie cookie = createCookie(request, name, "") - .maxAge(Duration.ZERO) - .path(path) - .build(); - addCookie(response, cookie); - } - - /** - * 응답에 쿠키 추가 - */ - private static void addCookie(HttpServletResponse response, ResponseCookie cookie) { - response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); - log.debug("Cookie set: name={}, path={}, maxAge={}, httpOnly={}, secure={}, sameSite={}", - cookie.getName(), cookie.getPath(), cookie.getMaxAge(), - cookie.isHttpOnly(), cookie.isSecure(), cookie.getSameSite()); - } -} diff --git a/src/main/java/com/grabpt/config/jwt/properties/CookieSupport.java b/src/main/java/com/grabpt/config/jwt/properties/CookieSupport.java deleted file mode 100644 index da2e8ebf..00000000 --- a/src/main/java/com/grabpt/config/jwt/properties/CookieSupport.java +++ /dev/null @@ -1,113 +0,0 @@ -package com.grabpt.config.jwt.properties; - -import java.time.Duration; - -import org.springframework.http.ResponseCookie; - -public final class CookieSupport { - - private CookieSupport() { - } - - public static ResponseCookie accessCookie(String token) { - return ResponseCookie.from("accessToken", token) - .httpOnly(true) // JS 접근 차단(XSS 방어) - .secure(true) // HTTPS 전용 - .sameSite("None") // same-site라 Lax면 충분 (필요시 "None") - .domain("grabpt.com") - .path("/") - .maxAge(60 * 15) // 예: 15분 - .build(); - } - - public static ResponseCookie refreshCookie(String token) { - return ResponseCookie.from("refreshToken", token) - .httpOnly(true) - .secure(true) - .sameSite("None") - .domain("grabpt.com") // 이미 사용 중인 도메인과 동일하게 - .path("/api/auth/reissue") - .maxAge(Duration.ofDays(7)) // 현재 유효기간 정책 유지 - .build(); - } - - /** role 쿠키 (프론트에서 읽어야 하므로 HttpOnly=false) */ - public static ResponseCookie roleCookie(String roleValueB64) { - return ResponseCookie.from("role", roleValueB64 == null ? "" : roleValueB64) - .httpOnly(false) // 프론트에서 읽음 - .secure(true) - .sameSite("None") - .domain("grabpt.com") - .path("/") - .maxAge(60 * 30) // 30분 (액세스 토큰 수명과 유사) - .build(); - } - - public static ResponseCookie deleteCookie(String name, String path) { - return ResponseCookie.from(name, "") - .httpOnly(true) - .secure(true) - .sameSite("None") - .domain("grabpt.com") - .path(path) - .maxAge(0) - .build(); - } - - /** 로그아웃 시 한번에 삭제할 세트 */ - public static ResponseCookie[] logoutDeletionSet() { - return new ResponseCookie[] { - deleteAccessCookie(), - deleteRefreshCookie(), - // role은 프론트에서 읽는 쿠키: 발급 속성(sameSite=None, path="/")에 맞춰 삭제 - ResponseCookie.from("role", "") - .httpOnly(false).secure(true).sameSite("None") - .domain("grabpt.com").path("/").maxAge(0).build() - }; - } - - public static ResponseCookie deleteAccessCookie() { - return ResponseCookie.from("accessToken", "") - .httpOnly(true).secure(true).sameSite("None") - .domain("grabpt.com").path("/") - .maxAge(0).build(); - } - - public static ResponseCookie deleteRefreshCookie() { - return ResponseCookie.from("refreshToken", "") - .httpOnly(true).secure(true).sameSite("None") - .domain("grabpt.com").path("/api/auth/reissue") - .maxAge(0).build(); - } - - public static ResponseCookie deleteRefreshCookieAtRoot() { - return ResponseCookie.from("refreshToken", "") - .httpOnly(true).secure(true).sameSite("None") - .domain("grabpt.com").path("/") - .maxAge(0).build(); - } - - public static ResponseCookie refreshCookieAtRoot(String token) { - return ResponseCookie.from("refreshToken", token) - .httpOnly(true).secure(true).sameSite("None") - .domain("grabpt.com").path("/") - .maxAge(Duration.ofDays(7)).build(); - } - - public static ResponseCookie userIdCookie(String valueB64) { - return ResponseCookie.from("userId", valueB64 == null ? "" : valueB64) - .httpOnly(false).secure(true).sameSite("None") - .domain("grabpt.com").path("/") - .maxAge(60 * 30).build(); - } - - public static ResponseCookie deleteCookieHostOnly(String name) { - return ResponseCookie.from(name, "") - .path("/") - .maxAge(0) // 즉시 만료 - .httpOnly(true) - .secure(true) // HTTPS 배포 환경 가정 - .sameSite("None") // cross-site 허용 - .build(); - } -} diff --git a/src/main/java/com/grabpt/config/oauth/handler/OAuth2SuccessHandler.java b/src/main/java/com/grabpt/config/oauth/handler/OAuth2SuccessHandler.java deleted file mode 100644 index 5688fef6..00000000 --- a/src/main/java/com/grabpt/config/oauth/handler/OAuth2SuccessHandler.java +++ /dev/null @@ -1,237 +0,0 @@ -package com.grabpt.config.oauth.handler; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.util.Base64; -import java.util.Map; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.ResponseCookie; -import org.springframework.security.core.Authentication; -import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; -import org.springframework.security.oauth2.core.user.OAuth2User; -import org.springframework.security.web.authentication.AuthenticationSuccessHandler; -import org.springframework.stereotype.Component; -import org.springframework.web.util.UriComponentsBuilder; - -import com.grabpt.config.jwt.JwtTokenProvider; -import com.grabpt.config.oauth.DynamicCookieSupport; -import com.grabpt.config.oauth.RedirectTargetResolver; -import com.grabpt.domain.entity.Users; -import com.grabpt.domain.enums.Role; -import com.grabpt.repository.UserRepository.UserRepository; - -import jakarta.servlet.ServletException; -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; - -@Slf4j -@Component -@RequiredArgsConstructor -public class OAuth2SuccessHandler implements AuthenticationSuccessHandler { - - private final JwtTokenProvider jwtTokenProvider; - private final UserRepository userRepository; - - // ===== helpers ===== - private static String b64(String s) { - if (s == null) - return ""; - return Base64.getEncoder().encodeToString(s.getBytes(StandardCharsets.UTF_8)); - } - - private void add(HttpServletResponse res, ResponseCookie c) { - res.addHeader(HttpHeaders.SET_COOKIE, c.toString()); - } - - private static String getCookieValue(HttpServletRequest request, String... names) { - Cookie[] cookies = request.getCookies(); - if (cookies == null) - return null; - for (String n : names) - for (Cookie c : cookies) - if (n.equals(c.getName())) - return c.getValue(); - return null; - } - - @SuppressWarnings("unchecked") - private static Map castMap(Object o) { - return (o instanceof Map m) ? (Map)m : null; - } - - private static String str(Object o) { - return o == null ? null : String.valueOf(o); - } - - /** dev/localhost 등 파라미터 방식으로 토큰/정보 전달이 필요한 대상 판정 */ - private static boolean needsParamTokens(String base) { - if (base == null) - return false; - String b = base.toLowerCase(); - return b.contains("localhost") - || b.contains("127.0.0.1") - || "https://grabpt-dev.vercel.app".equalsIgnoreCase(b); - } - - /** 과거/중복 쿠키 일괄 삭제 */ - private void deleteCookie(HttpServletResponse res, HttpServletRequest req, String name) { - add(res, DynamicCookieSupport.newCookie(name, "", req) - .maxAge(Duration.ZERO).build()); - add(res, DynamicCookieSupport.asPublic( - DynamicCookieSupport.newCookie(name, "", req)) - .maxAge(Duration.ZERO).build()); - } - - @Override - public void onAuthenticationSuccess(HttpServletRequest request, - HttpServletResponse response, - Authentication authentication) - throws IOException, ServletException { - - // 0) 최종 리다이렉트 대상(frontend base) 판별 - HttpSession session = request.getSession(false); - String sessionHint = session == null ? null : - (String)session.getAttribute(RedirectTargetResolver.REDIRECT_URI_COOKIE); - - String cookieHint = getCookieValue(request, - RedirectTargetResolver.REDIRECT_URI_COOKIE, - RedirectTargetResolver.ALT_REDIRECT_URI_COOKIE); - - String frontendBase = RedirectTargetResolver.resolveFrontendBase( - request, sessionHint != null ? sessionHint : cookieHint); - - if (!RedirectTargetResolver.isAllowedRedirectBase(frontendBase)) { - log.warn("Blocked unexpected redirect base: {}", frontendBase); - frontendBase = RedirectTargetResolver.EnvTarget.PROD_FE.base; - } - - // 힌트 소거 - if (session != null) - session.removeAttribute(RedirectTargetResolver.REDIRECT_URI_COOKIE); - add(response, DynamicCookieSupport.asPublic( - DynamicCookieSupport.newCookie(RedirectTargetResolver.REDIRECT_URI_COOKIE, "", request)) - .maxAge(Duration.ZERO).build()); - add(response, DynamicCookieSupport.asPublic( - DynamicCookieSupport.newCookie(RedirectTargetResolver.ALT_REDIRECT_URI_COOKIE, "", request)) - .maxAge(Duration.ZERO).build()); - - log.debug("[OAUTH][SUCCESS] frontendBase={} (sessionHint={}, cookieHint={})", - frontendBase, sessionHint, cookieHint); - log.debug("[OAUTH][SUCCESS] paramMode={}", needsParamTokens(frontendBase)); - - // 중복/레거시 쿠키 선삭제 - for (String n : new String[] {"access_token", "refresh_token", "ACCESS_TOKEN", "REFRESH_TOKEN"}) { - deleteCookie(response, request, n); - } - - // 1) 공급자/속성 파싱 - OAuth2User oAuth2User = (OAuth2User)authentication.getPrincipal(); - OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken)authentication; - String oauthProvider = oauthToken.getAuthorizedClientRegistrationId(); - - Map attributes = oAuth2User.getAttributes(); - String email = null, name = null, oauthId = null; - - if ("google".equals(oauthProvider)) { - email = str(attributes.get("email")); - name = str(attributes.get("name")); - oauthId = oauthProvider + "-" + str(attributes.get("sub")); - } else if ("kakao".equals(oauthProvider)) { - Map kakaoAccount = castMap(attributes.get("kakao_account")); - Map profile = kakaoAccount == null ? null : castMap(kakaoAccount.get("profile")); - email = kakaoAccount != null ? str(kakaoAccount.get("email")) : null; - name = profile != null ? str(profile.get("nickname")) : null; - oauthId = oauthProvider + "-" + str(attributes.get("id")); - } else if ("naver".equals(oauthProvider)) { - Map resp = castMap(attributes.get("response")); - email = resp != null ? str(resp.get("email")) : null; - name = resp != null ? str(resp.get("name")) : null; - oauthId = oauthProvider + "-" + (resp != null ? str(resp.get("id")) : null); - } - - // 2) 기존 회원 여부 - Users oauthUser = userRepository.findByOauthProviderAndOauthId(oauthProvider, oauthId).orElse(null); - - if (oauthUser != null) { - // === 기존 회원 === - String accessToken = jwtTokenProvider.generateToken(oauthUser); - String emailForRefresh = oauthUser.getEmail() != null ? oauthUser.getEmail() : email; - String newRefreshToken = jwtTokenProvider.createRefreshToken(emailForRefresh); - - oauthUser.setRefreshToken(newRefreshToken); - userRepository.save(oauthUser); - - add(response, DynamicCookieSupport.newCookie("ACCESS_TOKEN", accessToken, request) - .maxAge(Duration.ofHours(4)).build()); - add(response, DynamicCookieSupport.newCookie("REFRESH_TOKEN", newRefreshToken, request) - .maxAge(Duration.ofDays(30)).build()); - - String roleStr = oauthUser.getRole() == Role.PRO ? "PRO" : oauthUser.getRole().name(); - - add(response, DynamicCookieSupport.asPublic( - DynamicCookieSupport.newCookie("ROLE", b64(roleStr), request)) - .maxAge(Duration.ofDays(30)).build()); - add(response, DynamicCookieSupport.asPublic( - DynamicCookieSupport.newCookie("USER_ID", b64(oauthUser.getId().toString()), request)) - .maxAge(Duration.ofDays(30)).build()); - - org.springframework.security.core.context.SecurityContextHolder.clearContext(); - if (session != null) - session.invalidate(); - - // 리다이렉트 URL - String targetUrl; - if (needsParamTokens(frontendBase)) { - targetUrl = UriComponentsBuilder.fromUriString(frontendBase) - .path("/authcallback") - .queryParam("access_token", b64(accessToken)) - .queryParam("refresh_token", b64(newRefreshToken)) - .queryParam("role", b64(roleStr)) - .queryParam("user_id", b64(oauthUser.getId().toString())) - .build().toUriString(); - } else { - targetUrl = UriComponentsBuilder.fromUriString(frontendBase) - .path("/authcallback") - .build().toUriString(); - } - response.sendRedirect(targetUrl); - return; - } - - // === 신규 회원 === - String targetUrl; - if (needsParamTokens(frontendBase)) { - targetUrl = UriComponentsBuilder.fromUriString(frontendBase) - .path("/signup") - .queryParam("oauthEmail", b64(email)) - .queryParam("oauthName", b64(name)) - .queryParam("oauthId", b64(oauthId)) - .queryParam("oauthProvider", b64(oauthProvider)) - .build().toUriString(); - } else { - add(response, DynamicCookieSupport.asPublic( - DynamicCookieSupport.newCookie("oauthEmail", b64(email), request)) - .maxAge(Duration.ofMinutes(3)).build()); - add(response, DynamicCookieSupport.asPublic( - DynamicCookieSupport.newCookie("oauthName", b64(name), request)) - .maxAge(Duration.ofMinutes(3)).build()); - add(response, DynamicCookieSupport.asPublic( - DynamicCookieSupport.newCookie("oauthId", b64(oauthId), request)) - .maxAge(Duration.ofMinutes(3)).build()); - add(response, DynamicCookieSupport.asPublic( - DynamicCookieSupport.newCookie("oauthProvider", b64(oauthProvider), request)) - .maxAge(Duration.ofMinutes(3)).build()); - - targetUrl = UriComponentsBuilder.fromUriString(frontendBase) - .path("/signup") - .build().toUriString(); - } - response.sendRedirect(targetUrl); - } -} diff --git a/src/main/java/com/grabpt/config/oauth/handler/OAuth2SuccessHandlerFinal.java b/src/main/java/com/grabpt/config/oauth/handler/OAuth2SuccessHandlerFinal.java deleted file mode 100644 index 754d36d8..00000000 --- a/src/main/java/com/grabpt/config/oauth/handler/OAuth2SuccessHandlerFinal.java +++ /dev/null @@ -1,275 +0,0 @@ -package com.grabpt.config.oauth.handler; - -import java.io.IOException; -import java.util.Map; - -import org.springframework.security.core.Authentication; -import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; -import org.springframework.security.oauth2.core.user.OAuth2User; -import org.springframework.security.web.authentication.AuthenticationSuccessHandler; -import org.springframework.stereotype.Component; -import org.springframework.web.util.UriComponentsBuilder; - -import com.grabpt.config.jwt.JwtTokenProvider; -import com.grabpt.config.jwt.properties.CookieManagerV2; -import com.grabpt.config.oauth.EnvironmentDetector; -import com.grabpt.config.oauth.EnvironmentDetector.EnvironmentProfile; -import com.grabpt.config.oauth.RedirectTargetResolver; -import com.grabpt.domain.entity.Users; -import com.grabpt.domain.enums.Role; -import com.grabpt.repository.UserRepository.UserRepository; - -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.servlet.http.HttpSession; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -@Component -@RequiredArgsConstructor -public class OAuth2SuccessHandlerFinal implements AuthenticationSuccessHandler { - - private final JwtTokenProvider jwtTokenProvider; - private final UserRepository userRepository; - - @Override - public void onAuthenticationSuccess(HttpServletRequest request, - HttpServletResponse response, - Authentication authentication) - throws IOException, ServletException { - - // 1. 리다이렉트 대상 및 환경 결정 - String frontendBase = resolveFrontendBase(request); - EnvironmentProfile env = EnvironmentDetector.detectEnvironment(frontendBase); - - log.info("[OAuth Success] Frontend: {}, Environment: {}", frontendBase, env.env); - - // 2. OAuth 정보 파싱 - OAuthUserInfo oauthInfo = extractOAuthInfo(authentication); - log.info("[OAuth Success] Provider: {}, Email: {}", oauthInfo.provider, oauthInfo.email); - - // 3. 기존 회원 확인 - Users existingUser = userRepository - .findByOauthProviderAndOauthId(oauthInfo.provider, oauthInfo.oauthId) - .orElse(null); - - if (existingUser != null) { - // 기존 회원 - 토큰 발급 및 로그인 처리 - handleExistingUser(request, response, existingUser, frontendBase, env); - } else { - // 신규 회원 - 회원가입 페이지로 리다이렉트 - handleNewUser(request, response, oauthInfo, frontendBase, env); - } - - // 4. 세션 정리 - cleanupSession(request); - } - - /** - * 기존 회원 로그인 처리 - */ - private void handleExistingUser(HttpServletRequest request, HttpServletResponse response, - Users user, String frontendBase, EnvironmentProfile env) - throws IOException { - - // JWT 생성 - String accessToken = jwtTokenProvider.generateToken(user); - String refreshToken = jwtTokenProvider.createRefreshToken( - user.getEmail() != null ? user.getEmail() : user.getOauthId() - ); - - // DB에 Refresh Token 저장 - user.setRefreshToken(refreshToken); - userRepository.save(user); - - // 쿠키 설정 (환경별 자동 처리) - CookieManagerV2.setAccessToken(response, request, accessToken); - CookieManagerV2.setRefreshToken(response, request, refreshToken); - CookieManagerV2.setRole(response, request, getRoleString(user.getRole())); - CookieManagerV2.setUserId(response, request, user.getId().toString()); - - log.info("[OAuth Success] Existing user logged in: userId={}, role={}, env={}", - user.getId(), user.getRole(), env.env); - - // 리다이렉트 URL 생성 - String redirectUrl = buildRedirectUrl( - frontendBase, - "/authcallback", - env.useUrlParams, - accessToken, - refreshToken, - getRoleString(user.getRole()), - user.getId().toString(), - null, null, null, null // OAuth 정보 없음 - ); - - response.sendRedirect(redirectUrl); - } - - /** - * 신규 회원 처리 - 회원가입 페이지로 리다이렉트 - */ - private void handleNewUser(HttpServletRequest request, HttpServletResponse response, - OAuthUserInfo oauthInfo, String frontendBase, - EnvironmentProfile env) throws IOException { - - // 환경별 OAuth 정보 전달 방식 - if (env.useUrlParams) { - // LOCAL/DEV: URL 파라미터로 전달 - log.info("[OAuth Success] New user (DEV mode): redirecting with URL params"); - } else { - // PROD: HttpOnly 쿠키로 전달 - CookieManagerV2.setOAuthTempInfo(response, request, - oauthInfo.email, - oauthInfo.name, - oauthInfo.oauthId, - oauthInfo.provider - ); - log.info("[OAuth Success] New user (PROD mode): redirecting with cookies"); - } - - // 회원가입 페이지로 리다이렉트 - String redirectUrl = buildRedirectUrl( - frontendBase, - "/signup", - env.useUrlParams, - null, null, null, null, // JWT 정보 없음 - oauthInfo.email, - oauthInfo.name, - oauthInfo.oauthId, - oauthInfo.provider - ); - - response.sendRedirect(redirectUrl); - } - - /** - * 리다이렉트 URL 생성 (환경별 자동 처리) - * - * @param useUrlParams true면 URL 파라미터 추가, false면 쿠키만 사용 - */ - private String buildRedirectUrl(String frontendBase, String path, boolean useUrlParams, - String accessToken, String refreshToken, String role, String userId, - String oauthEmail, String oauthName, String oauthId, String oauthProvider) { - - UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(frontendBase) - .path(path); - - // URL 파라미터로 전달 (개발 환경) - if (useUrlParams) { - if (accessToken != null) { - builder.queryParam("access_token", accessToken) - .queryParam("refresh_token", refreshToken) - .queryParam("role", role) - .queryParam("user_id", userId); - } - if (oauthEmail != null) { - builder.queryParam("oauthEmail", oauthEmail) - .queryParam("oauthName", oauthName) - .queryParam("oauthId", oauthId) - .queryParam("oauthProvider", oauthProvider); - } - } - // 쿠키로만 전달 (운영 환경) - 별도 작업 불필요 - - return builder.build().toUriString(); - } - - /** - * OAuth 사용자 정보 추출 - */ - private OAuthUserInfo extractOAuthInfo(Authentication authentication) { - OAuth2User oAuth2User = (OAuth2User)authentication.getPrincipal(); - OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken)authentication; - String provider = oauthToken.getAuthorizedClientRegistrationId(); - Map attributes = oAuth2User.getAttributes(); - - return switch (provider) { - case "google" -> extractGoogleInfo(attributes, provider); - case "kakao" -> extractKakaoInfo(attributes, provider); - case "naver" -> extractNaverInfo(attributes, provider); - default -> throw new IllegalArgumentException("Unsupported OAuth provider: " + provider); - }; - } - - private OAuthUserInfo extractGoogleInfo(Map attributes, String provider) { - return new OAuthUserInfo( - provider, - String.valueOf(attributes.get("email")), - String.valueOf(attributes.get("name")), - provider + "-" + attributes.get("sub") - ); - } - - @SuppressWarnings("unchecked") - private OAuthUserInfo extractKakaoInfo(Map attributes, String provider) { - Map kakaoAccount = (Map)attributes.get("kakao_account"); - Map profile = kakaoAccount != null - ? (Map)kakaoAccount.get("profile") - : null; - - String email = kakaoAccount != null ? String.valueOf(kakaoAccount.get("email")) : null; - String name = profile != null ? String.valueOf(profile.get("nickname")) : null; - String oauthId = provider + "-" + attributes.get("id"); - - return new OAuthUserInfo(provider, email, name, oauthId); - } - - @SuppressWarnings("unchecked") - private OAuthUserInfo extractNaverInfo(Map attributes, String provider) { - Map response = (Map)attributes.get("response"); - - String email = response != null ? String.valueOf(response.get("email")) : null; - String name = response != null ? String.valueOf(response.get("name")) : null; - String oauthId = provider + "-" + (response != null ? response.get("id") : null); - - return new OAuthUserInfo(provider, email, name, oauthId); - } - - /** - * 프론트엔드 Base URL 결정 - */ - private String resolveFrontendBase(HttpServletRequest request) { - HttpSession session = request.getSession(false); - String sessionHint = session != null - ? (String)session.getAttribute(RedirectTargetResolver.REDIRECT_URI_COOKIE) - : null; - - String frontendBase = RedirectTargetResolver.resolveFrontendBase(request, sessionHint); - - if (!RedirectTargetResolver.isAllowedRedirectBase(frontendBase)) { - log.warn("[OAuth Success] Blocked unexpected redirect: {}", frontendBase); - frontendBase = RedirectTargetResolver.EnvTarget.PROD_FE.base; - } - - return frontendBase; - } - - /** - * Role을 문자열로 변환 - */ - private String getRoleString(Role role) { - return role == Role.PRO ? "PRO" : role.name(); - } - - /** - * 세션 정리 - */ - private void cleanupSession(HttpServletRequest request) { - HttpSession session = request.getSession(false); - if (session != null) { - session.removeAttribute(RedirectTargetResolver.REDIRECT_URI_COOKIE); - } - - org.springframework.security.core.context.SecurityContextHolder.clearContext(); - } - - /** - * OAuth 사용자 정보 DTO - */ - private record OAuthUserInfo(String provider, String email, String name, String oauthId) { - } -} - diff --git a/src/main/java/com/grabpt/service/UserService/UserQueryServiceImpl.java b/src/main/java/com/grabpt/service/UserService/UserQueryServiceImpl.java index c071a391..f723e74a 100644 --- a/src/main/java/com/grabpt/service/UserService/UserQueryServiceImpl.java +++ b/src/main/java/com/grabpt/service/UserService/UserQueryServiceImpl.java @@ -11,7 +11,7 @@ import com.grabpt.apiPayload.exception.handler.UserHandler; import com.grabpt.config.SecurityUtils; import com.grabpt.config.auth.PrincipalDetails; -import com.grabpt.config.jwt.JwtTokenProvider; +import com.grabpt.config.jwt.JwtTokenProviderImproved; import com.grabpt.converter.UserConverter; import com.grabpt.domain.entity.Users; import com.grabpt.dto.response.UserResponseDto; @@ -27,7 +27,7 @@ public class UserQueryServiceImpl implements UserQueryService { private final UserRepository userRepository; - private final JwtTokenProvider jwtTokenProvider; + private final JwtTokenProviderImproved jwtTokenProvider; @Override @Transactional(readOnly = true)