diff --git a/api/src/main/java/com/pinback/api/google/controller/GoogleLoginControllerV3.java b/api/src/main/java/com/pinback/api/google/controller/GoogleLoginControllerV3.java index 1b69c7b..516c253 100644 --- a/api/src/main/java/com/pinback/api/google/controller/GoogleLoginControllerV3.java +++ b/api/src/main/java/com/pinback/api/google/controller/GoogleLoginControllerV3.java @@ -7,7 +7,7 @@ import org.springframework.web.bind.annotation.RestController; import com.pinback.api.auth.dto.request.SignUpRequestV3; -import com.pinback.api.google.dto.request.GoogleLoginRequest; +import com.pinback.api.google.dto.request.GoogleLoginRequestV3; import com.pinback.application.auth.dto.SignUpResponse; import com.pinback.application.auth.usecase.AuthUsecase; import com.pinback.application.google.dto.response.GoogleLoginResponseV3; @@ -31,9 +31,9 @@ public class GoogleLoginControllerV3 { @Operation(summary = "구글 소셜 로그인 V3", description = "구글 소셜 로그인을 진행하며, 응답에 직무 선택 여부를 포함합니다.") @PostMapping("/google") public Mono> googleLogin( - @Valid @RequestBody GoogleLoginRequest request + @Valid @RequestBody GoogleLoginRequestV3 request ) { - return googleUsecase.getUserInfo(request.toCommand()) + return googleUsecase.getUserInfoV3(request.toCommand()) .flatMap(googleResponse -> { return authUsecase.getInfoAndTokenV3(googleResponse.email(), googleResponse.pictureUrl(), googleResponse.name()) diff --git a/api/src/main/java/com/pinback/api/google/dto/request/GoogleLoginRequestV3.java b/api/src/main/java/com/pinback/api/google/dto/request/GoogleLoginRequestV3.java new file mode 100644 index 0000000..dabb1ed --- /dev/null +++ b/api/src/main/java/com/pinback/api/google/dto/request/GoogleLoginRequestV3.java @@ -0,0 +1,16 @@ +package com.pinback.api.google.dto.request; + +import com.pinback.application.google.dto.GoogleLoginCommandV3; + +import jakarta.validation.constraints.NotNull; + +public record GoogleLoginRequestV3( + @NotNull(message = "인가 코드(code)는 비어있을 수 없습니다.") + String code, + @NotNull(message = "구글 로그인 리다이렉션 uri는 비어있을 수 없습니다.") + String uri +) { + public GoogleLoginCommandV3 toCommand() { + return new GoogleLoginCommandV3(code, uri); + } +} diff --git a/api/src/main/resources/application.yml b/api/src/main/resources/application.yml index 50d780d..714d2d0 100644 --- a/api/src/main/resources/application.yml +++ b/api/src/main/resources/application.yml @@ -31,7 +31,7 @@ fcm: ${FCM_JSON} google: client-id: ${CLIENT_ID} client-secret: ${CLIENT_SECRET} - redirect-uri: ${REDIRECT_URI} + redirect-uris: ${REDIRECT_URI},${REDIRECT_URI_LOCAL1},${REDIRECT_URI_LOCAL2} token-uri: ${TOKEN_URI} user-info-uri: ${USER_INFO_URI} diff --git a/application/src/main/java/com/pinback/application/common/exception/InvalidGoogleUriException.java b/application/src/main/java/com/pinback/application/common/exception/InvalidGoogleUriException.java new file mode 100644 index 0000000..7220f65 --- /dev/null +++ b/application/src/main/java/com/pinback/application/common/exception/InvalidGoogleUriException.java @@ -0,0 +1,10 @@ +package com.pinback.application.common.exception; + +import com.pinback.shared.constant.ExceptionCode; +import com.pinback.shared.exception.ApplicationException; + +public class InvalidGoogleUriException extends ApplicationException { + public InvalidGoogleUriException() { + super(ExceptionCode.INVALID_REDIRECT_URI); + } +} diff --git a/application/src/main/java/com/pinback/application/google/dto/GoogleLoginCommandV3.java b/application/src/main/java/com/pinback/application/google/dto/GoogleLoginCommandV3.java new file mode 100644 index 0000000..83d78f6 --- /dev/null +++ b/application/src/main/java/com/pinback/application/google/dto/GoogleLoginCommandV3.java @@ -0,0 +1,7 @@ +package com.pinback.application.google.dto; + +public record GoogleLoginCommandV3( + String code, + String uri +) { +} diff --git a/application/src/main/java/com/pinback/application/google/port/out/GoogleOAuthPort.java b/application/src/main/java/com/pinback/application/google/port/out/GoogleOAuthPort.java index 13d3fc0..54405ab 100644 --- a/application/src/main/java/com/pinback/application/google/port/out/GoogleOAuthPort.java +++ b/application/src/main/java/com/pinback/application/google/port/out/GoogleOAuthPort.java @@ -6,4 +6,6 @@ public interface GoogleOAuthPort { Mono fetchUserInfo(String code); + + Mono fetchUserInfoV3(String code, String uri); } diff --git a/application/src/main/java/com/pinback/application/google/service/GoogleOAuthClient.java b/application/src/main/java/com/pinback/application/google/service/GoogleOAuthClient.java index fe517a9..c975c02 100644 --- a/application/src/main/java/com/pinback/application/google/service/GoogleOAuthClient.java +++ b/application/src/main/java/com/pinback/application/google/service/GoogleOAuthClient.java @@ -1,5 +1,7 @@ package com.pinback.application.google.service; +import java.util.List; + import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; @@ -10,6 +12,7 @@ import com.pinback.application.common.exception.GoogleNameMissingException; import com.pinback.application.common.exception.GoogleProfileImageMissingException; import com.pinback.application.common.exception.GoogleTokenMissingException; +import com.pinback.application.common.exception.InvalidGoogleUriException; import com.pinback.application.google.dto.response.GoogleApiResponse; import com.pinback.application.google.dto.response.GoogleTokenResponse; import com.pinback.application.google.dto.response.GoogleUserInfoResponse; @@ -24,7 +27,7 @@ public class GoogleOAuthClient implements GoogleOAuthPort { private final WebClient googleWebClient; private final String googleClientId; private final String googleClientSecret; - private final String googleRedirectUri; + private final List googleRedirectUris; private final String googleTokenUri; private final String googleUserInfoUri; @@ -32,13 +35,13 @@ public GoogleOAuthClient( WebClient googleWebClient, @Qualifier("googleClientId") String googleClientId, @Qualifier("googleClientSecret") String googleClientSecret, - @Qualifier("googleRedirectUri") String googleRedirectUri, + @Qualifier("googleRedirectUris") List googleRedirectUris, @Qualifier("googleTokenUri") String googleTokenUri, @Qualifier("googleUserInfoUri") String googleUserInfoUri) { this.googleWebClient = googleWebClient; this.googleClientId = googleClientId; this.googleClientSecret = googleClientSecret; - this.googleRedirectUri = googleRedirectUri; + this.googleRedirectUris = googleRedirectUris; this.googleTokenUri = googleTokenUri; this.googleUserInfoUri = googleUserInfoUri; } @@ -61,12 +64,65 @@ public Mono fetchUserInfo(String code) { }); } + @Override + public Mono fetchUserInfoV3(String code, String uri) { + + return requestAccessTokenV3(code, uri) + // 토큰 응답을 UserInfo 요청으로 변환하여 연결 + .flatMap(tokenResponse -> { + + // Access Token 유효성 검증 + if (tokenResponse == null || tokenResponse.accessToken() == null) { + log.info("tokenResponse: {}", tokenResponse); + log.error("Google Access Token 획득 실패: 응답 본문에 토큰이 없습니다. Code: {}", code); + return Mono.error(new GoogleTokenMissingException()); + } + // Access Token으로 사용자 정보 요청 + return getUserInfo(tokenResponse.accessToken()); + }); + } + private Mono requestAccessToken(String code) { - log.info("redirect: {}", googleRedirectUri); + String firstRedirectUri = googleRedirectUris.getFirst(); + log.info("redirect: {}", firstRedirectUri); + String requestBody = "code=" + code + + "&client_id=" + googleClientId + + "&client_secret=" + googleClientSecret + + "&redirect_uri=" + firstRedirectUri + + "&grant_type=authorization_code"; + + return googleWebClient.post() + .uri(googleTokenUri) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .bodyValue(requestBody) + .retrieve() + // HTTP 오류 발생 시 + .onStatus(status -> status.isError(), clientResponse -> + clientResponse.bodyToMono(String.class) + .flatMap(body -> { + String errorLog = String.format( + "[GoogleOAuth API 에러] HTTP Status: %s, Detail: %s", + clientResponse.statusCode(), body + ); + log.error(errorLog); + return Mono.error(new GoogleApiException()); + }) + ) + .bodyToMono(GoogleTokenResponse.class); + } + + private Mono requestAccessTokenV3(String code, String uri) { + if (!googleRedirectUris.contains(uri)) { + log.error("허용되지 않은 Redirect URI 요청: {}", uri); + + return Mono.error(new InvalidGoogleUriException()); + } + + log.info("redirect: {}", uri); String requestBody = "code=" + code + "&client_id=" + googleClientId + "&client_secret=" + googleClientSecret + - "&redirect_uri=" + googleRedirectUri + + "&redirect_uri=" + uri + "&grant_type=authorization_code"; return googleWebClient.post() diff --git a/application/src/main/java/com/pinback/application/google/usecase/GoogleUsecase.java b/application/src/main/java/com/pinback/application/google/usecase/GoogleUsecase.java index d064d45..c99d58d 100644 --- a/application/src/main/java/com/pinback/application/google/usecase/GoogleUsecase.java +++ b/application/src/main/java/com/pinback/application/google/usecase/GoogleUsecase.java @@ -3,6 +3,7 @@ import org.springframework.stereotype.Service; import com.pinback.application.google.dto.GoogleLoginCommand; +import com.pinback.application.google.dto.GoogleLoginCommandV3; import com.pinback.application.google.dto.response.GoogleUserInfoResponse; import com.pinback.application.google.port.out.GoogleOAuthPort; @@ -20,4 +21,12 @@ public Mono getUserInfo(GoogleLoginCommand command) { return googleOAuthPort.fetchUserInfo(code); } + + public Mono getUserInfoV3(GoogleLoginCommandV3 command) { + + String code = command.code(); + String uri = command.uri(); + + return googleOAuthPort.fetchUserInfoV3(code, uri); + } } diff --git a/infrastructure/src/main/java/com/pinback/infrastructure/config/google/GoogleConfig.java b/infrastructure/src/main/java/com/pinback/infrastructure/config/google/GoogleConfig.java index 116474b..b0e13b9 100644 --- a/infrastructure/src/main/java/com/pinback/infrastructure/config/google/GoogleConfig.java +++ b/infrastructure/src/main/java/com/pinback/infrastructure/config/google/GoogleConfig.java @@ -1,5 +1,7 @@ package com.pinback.infrastructure.config.google; +import java.util.List; + import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -14,8 +16,8 @@ public class GoogleConfig { @Value("${google.client-secret}") private String clientSecret; - @Value("${google.redirect-uri}") - private String redirectUri; + @Value("${google.redirect-uris}") + private List redirectUris; @Value("${google.token-uri}") private String tokenUri; @@ -39,8 +41,8 @@ public String googleClientSecret() { } @Bean - public String googleRedirectUri() { - return redirectUri; + public List googleRedirectUris() { + return redirectUris; } @Bean diff --git a/shared/src/main/java/com/pinback/shared/constant/ExceptionCode.java b/shared/src/main/java/com/pinback/shared/constant/ExceptionCode.java index 9079ca1..a1e9c9a 100644 --- a/shared/src/main/java/com/pinback/shared/constant/ExceptionCode.java +++ b/shared/src/main/java/com/pinback/shared/constant/ExceptionCode.java @@ -15,6 +15,7 @@ public enum ExceptionCode { INVALID_FCM_TOKEN(HttpStatus.BAD_REQUEST, "c40004", "유효하지 않은 FCM 토큰입니다."), INVALID_URL(HttpStatus.BAD_REQUEST, "c40005", "유효하지 않은 URL이거나 접속할 수 없는 사이트입니다."), INVALID_READSTATUS(HttpStatus.BAD_REQUEST, "c40006", "잘못된 read-status 상태 값입니다.(전체보기: 생략/안읽음: false)"), + INVALID_REDIRECT_URI(HttpStatus.BAD_REQUEST, "c40007", "등록되지 않은 리다이렉트 uri 입니다"), //401 INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "c40101", "유효하지 않은 토큰입니다."),