Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -31,9 +31,9 @@ public class GoogleLoginControllerV3 {
@Operation(summary = "구글 소셜 로그인 V3", description = "구글 소셜 로그인을 진행하며, 응답에 직무 선택 여부를 포함합니다.")
@PostMapping("/google")
public Mono<ResponseDto<GoogleLoginResponseV3>> 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())
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
2 changes: 1 addition & 1 deletion api/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.pinback.application.google.dto;

public record GoogleLoginCommandV3(
String code,
String uri
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@

public interface GoogleOAuthPort {
Mono<GoogleUserInfoResponse> fetchUserInfo(String code);

Mono<GoogleUserInfoResponse> fetchUserInfoV3(String code, String uri);
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -24,21 +27,21 @@ public class GoogleOAuthClient implements GoogleOAuthPort {
private final WebClient googleWebClient;
private final String googleClientId;
private final String googleClientSecret;
private final String googleRedirectUri;
private final List<String> googleRedirectUris;
private final String googleTokenUri;
private final String googleUserInfoUri;

public GoogleOAuthClient(
WebClient googleWebClient,
@Qualifier("googleClientId") String googleClientId,
@Qualifier("googleClientSecret") String googleClientSecret,
@Qualifier("googleRedirectUri") String googleRedirectUri,
@Qualifier("googleRedirectUris") List<String> 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;
}
Expand All @@ -61,12 +64,65 @@ public Mono<GoogleUserInfoResponse> fetchUserInfo(String code) {
});
}

@Override
public Mono<GoogleUserInfoResponse> 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<GoogleTokenResponse> 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<GoogleTokenResponse> 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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -20,4 +21,12 @@ public Mono<GoogleUserInfoResponse> getUserInfo(GoogleLoginCommand command) {

return googleOAuthPort.fetchUserInfo(code);
}

public Mono<GoogleUserInfoResponse> getUserInfoV3(GoogleLoginCommandV3 command) {

String code = command.code();
String uri = command.uri();

return googleOAuthPort.fetchUserInfoV3(code, uri);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<String> redirectUris;

@Value("${google.token-uri}")
private String tokenUri;
Expand All @@ -39,8 +41,8 @@ public String googleClientSecret() {
}

@Bean
public String googleRedirectUri() {
return redirectUri;
public List<String> googleRedirectUris() {
return redirectUris;
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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", "유효하지 않은 토큰입니다."),
Expand Down