Skip to content

Commit

Permalink
Merge pull request #43 from Tico-Corp/feature-be/TICO-229-update-resp…
Browse files Browse the repository at this point in the history
…onse-handling

[FEAT] 구글 idToken 요청 로직 변경 및 에러 응답 표준화 (TICO-229)
  • Loading branch information
bu119 authored Jul 19, 2024
2 parents 33f0734 + 90e13b8 commit 12a9cdc
Show file tree
Hide file tree
Showing 11 changed files with 235 additions and 75 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
Expand All @@ -28,29 +30,29 @@
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/auth")
@Slf4j
public class AuthController {
//사용자 로그인
//사용자 로그아웃
//액세스 토큰 및 리프레시 토큰 발급
//토큰 갱신
// 사용자 로그인
// 사용자 로그아웃
// 액세스 토큰 및 리프레시 토큰 발급
// 토큰 갱신

private final AuthService authService;


/**
* 구글 로그인 API
*
* @param authorizationHeader Authorization 헤더에는 구글 ID 토큰이 포함됩니다.
* @param googleIdTokenHeader Google-ID-Token 헤더에는 구글 ID 토큰이 포함됩니다.
* @return 성공 시 JwtDTO를 포함하는 SuccessResponseDTO
* @throws CustomException 구글 ID 토큰 검증에 실패한 경우 예외를 던집니다.
*/
@Operation(
summary = "로그인",
description = "구글 소셜 로그인을 통해 로그인을 수행합니다. <br>"
+ "Authorization 헤더에는 구글 ID 토큰을 입력해야 합니다. <br>"
+ "Google-ID-Token 헤더에는 구글 ID 토큰을 입력해야 합니다. 예시: Bearer 차돌이짱귀여워 <br>"
+ "로그인 성공 시에는 JwtDTO를 포함하는 SuccessResponseDTO를 반환합니다.",
parameters = @Parameter(
name = "Authorization",
name = "Google-ID-Token",
description = "Google ID Token",
in = ParameterIn.HEADER,
required = true
Expand All @@ -60,41 +62,42 @@ public class AuthController {
@ApiResponse(responseCode = "200", description = "로그인 성공"),
@ApiResponse(responseCode = "401", description = "구글 ID 토큰이 유효하지 않음",
content = @Content(schema = @Schema(implementation = ErrorResponseEntity.class))),
@ApiResponse(responseCode = "400", description = "Authorization 헤더의 토큰이 유효하지 않음",
@ApiResponse(responseCode = "400", description = "Google-ID-Token 헤더의 토큰이 유효하지 않음",
content = @Content(schema = @Schema(implementation = ErrorResponseEntity.class))),
@ApiResponse(responseCode = "404", description = "등록되지 않은 사용자 (code: -104)",
@ApiResponse(responseCode = "404", description = "등록되지 않은 사용자 (code: U-104)",
content = @Content(schema = @Schema(implementation = ErrorResponseEntity.class)))
})
@PostMapping("/google/login")
public ResponseEntity<SuccessResponseDTO<JwtDTO>> googleLogin(@RequestHeader("Authorization") String authorizationHeader) {
public ResponseEntity<SuccessResponseDTO<JwtDTO>> googleLogin(@RequestHeader("Google-ID-Token") String googleIdTokenHeader) {
try {
JwtDTO jwtResponse = authService.googleLogin(authorizationHeader);
JwtDTO jwtResponse = authService.googleLogin(googleIdTokenHeader);
SuccessResponseDTO<JwtDTO> response = SuccessResponseDTO.<JwtDTO>builder()
.status(CustomSuccessCode.GOOGLE_LOGIN_SUCCESS.getHttpStatus().value())
.message(CustomSuccessCode.GOOGLE_LOGIN_SUCCESS.getMessage())
.data(jwtResponse)
.build();
return ResponseEntity.ok(response);
} catch (GeneralSecurityException | IOException e) {
} catch (GeneralSecurityException | IOException | IllegalArgumentException e) {
log.error("Google ID Token verification failed: {}", e.getMessage(), e);
throw new CustomException(CustomErrorCode.GOOGLE_TOKEN_VERIFICATION_FAILED);
}
}

/**
* 구글 회원가입 API
*
* @param authorizationHeader Authorization 헤더에는 구글 ID 토큰이 포함됩니다.
* @param googleIdTokenHeader Google-ID-Token 헤더에는 구글 ID 토큰이 포함됩니다.
* @param requestUserInfo 회원가입 요청 정보가 포함된 DTO
* @return 성공 시 JwtDTO를 포함하는 SuccessResponseDTO
* @throws CustomException 구글 ID 토큰 검증에 실패한 경우 예외를 던집니다.
*/
@Operation(
summary = "회원 가입",
description = "구글 소셜 로그인을 통해 회원가입을 수행합니다. <br>"
+ "Authorization 헤더에는 구글 ID 토큰을 입력하고, 요청 본문에는 닉네임 등의 추가 정보를 포함해야 합니다. <br>"
+ "Google-ID-Token 헤더에는 구글 ID 토큰을 입력하고, 요청 본문에는 닉네임 등의 추가 정보를 포함해야 합니다. <br>"
+ "회원가입 성공 시에는 JwtDTO를 포함하는 SuccessResponseDTO를 반환합니다.",
parameters = @Parameter(
name = "Authorization",
name = "Google-ID-Token",
description = "Google ID Token",
in = ParameterIn.HEADER,
required = true
Expand All @@ -104,24 +107,25 @@ public ResponseEntity<SuccessResponseDTO<JwtDTO>> googleLogin(@RequestHeader("Au
@ApiResponse(responseCode = "201", description = "회원가입 성공"),
@ApiResponse(responseCode = "401", description = "구글 ID 토큰이 유효하지 않음",
content = @Content(schema = @Schema(implementation = ErrorResponseEntity.class))),
@ApiResponse(responseCode = "400", description = "Authorization 헤더의 토큰이 유효하지 않음 또는 요청 본문이 잘못됨",
@ApiResponse(responseCode = "400", description = "Google-ID-Token 헤더의 토큰이 유효하지 않음 또는 요청 본문이 잘못됨",
content = @Content(schema = @Schema(implementation = ErrorResponseEntity.class))),
@ApiResponse(responseCode = "409", description = "이미 등록된 사용자 (code: -105)",
@ApiResponse(responseCode = "409", description = "이미 등록된 사용자 (code: U-105)",
content = @Content(schema = @Schema(implementation = ErrorResponseEntity.class)))
})
@PostMapping("/google/join")
public ResponseEntity<SuccessResponseDTO<JwtDTO>> googleJoin(@RequestHeader("Authorization") String authorizationHeader,
@RequestBody GoogleJoinDTO requestUserInfo) {
public ResponseEntity<SuccessResponseDTO<JwtDTO>> googleJoin(@RequestHeader("Google-ID-Token") String googleIdTokenHeader,
@Valid @RequestBody GoogleJoinDTO requestUserInfo) {
try {
JwtDTO jwtResponse = authService.googleJoin(authorizationHeader, requestUserInfo);
JwtDTO jwtResponse = authService.googleJoin(googleIdTokenHeader, requestUserInfo);
SuccessResponseDTO<JwtDTO> response = SuccessResponseDTO.<JwtDTO>builder()
.status(CustomSuccessCode.GOOGLE_SIGNUP_SUCCESS.getHttpStatus().value())
.message(CustomSuccessCode.GOOGLE_SIGNUP_SUCCESS.getMessage())
.data(jwtResponse)
.build();
return ResponseEntity.status(HttpStatus.CREATED).body(response);
} catch (GeneralSecurityException | IOException e) {
} catch (GeneralSecurityException | IOException | IllegalArgumentException e) {
log.error("Google ID Token verification failed: {}", e.getMessage(), e);
throw new CustomException(CustomErrorCode.GOOGLE_TOKEN_VERIFICATION_FAILED);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,10 @@

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import org.springframework.validation.annotation.Validated;


@Getter
@Validated // 유효성 검사를 위해 추가
@Schema(description = "Google Join Info")
public class GoogleJoinDTO {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.tico.pomoro_do.domain.user.dto.request.GoogleJoinDTO;
import com.tico.pomoro_do.domain.user.dto.response.JwtDTO;
import com.tico.pomoro_do.domain.user.entity.User;
import com.tico.pomoro_do.global.common.enums.TokenType;
import com.tico.pomoro_do.global.common.enums.UserRole;

import java.io.IOException;
Expand All @@ -13,13 +14,16 @@
public interface AuthService {

// 구글 ID 토큰 무결성 검사
GoogleUserInfoDTO verifyGoogleIdToken(String idToken) throws GeneralSecurityException, IOException;
GoogleUserInfoDTO verifyGoogleIdToken(String idToken) throws GeneralSecurityException, IOException, IllegalArgumentException;

// 토큰 형태 검사
String extractToken(String header, TokenType tokenType);

// 구글 로그인
JwtDTO googleLogin(String authorizationHeader) throws GeneralSecurityException, IOException;
JwtDTO googleLogin(String idTokenHeader) throws GeneralSecurityException, IOException;

// 구글 회원가입
JwtDTO googleJoin(String authorizationHeader, GoogleJoinDTO request) throws GeneralSecurityException, IOException;
JwtDTO googleJoin(String idTokenHeader, GoogleJoinDTO request) throws GeneralSecurityException, IOException;

// User 생성
User createUser(String username, String nickname, String profileImageUrl, UserRole role);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
import com.tico.pomoro_do.domain.user.repository.UserRepository;
import com.tico.pomoro_do.global.auth.jwt.JWTUtil;
import com.tico.pomoro_do.global.common.enums.SocialProvider;
import com.tico.pomoro_do.global.common.enums.TokenType;
import com.tico.pomoro_do.global.common.enums.UserRole;
import com.tico.pomoro_do.global.common.enums.UserStatus;
import com.tico.pomoro_do.global.exception.CustomErrorCode;
import com.tico.pomoro_do.global.exception.CustomException;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -57,14 +57,14 @@ public class AuthServiceImpl implements AuthService {
* @throws CustomException 구글 ID 토큰이 유효하지 않은 경우 예외
*/
@Override
public GoogleUserInfoDTO verifyGoogleIdToken(String idToken) throws GeneralSecurityException, IOException {
public GoogleUserInfoDTO verifyGoogleIdToken(String idToken) throws GeneralSecurityException, IOException, IllegalArgumentException {
NetHttpTransport transport = new NetHttpTransport();
JsonFactory jsonFactory = new GsonFactory();
GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(transport, jsonFactory)
.setAudience(Collections.singletonList(clientId))
.build();

GoogleIdToken googleIdToken = verifier.verify(idToken);
GoogleIdToken googleIdToken = verifier.verify(idToken); // 검증 실패시 IllegalArgumentException를 던짐
if (googleIdToken != null) {
GoogleIdToken.Payload payload = googleIdToken.getPayload();
return GoogleUserInfoDTO.builder()
Expand All @@ -74,7 +74,7 @@ public GoogleUserInfoDTO verifyGoogleIdToken(String idToken) throws GeneralSecur
.pictureUrl((String) payload.get("picture"))
.build();
} else {
throw new CustomException(CustomErrorCode.GOOGLE_TOKEN_INVALID);
throw new CustomException(CustomErrorCode.GOOGLE_TOKEN_VERIFICATION_FAILED);
}
}

Expand All @@ -94,32 +94,43 @@ public JwtDTO createJwtTokens(String email, String role) {
}

/**
* Authorization 헤더에서 토큰 추출
* 토큰 관련 헤더에서 토큰 값을 추출합니다.
*
* @param authorizationHeader Authorization 헤더
* @return 추출된 토큰
* @throws CustomException Authorization 헤더가 유효하지 않은 경우 예외
* @param header 토큰 헤더 (예: "Bearer <token>")
* @param tokenType 토큰의 타입 (Google ID 토큰 또는 JWT)
* @return 추출된 토큰 값
* @throws CustomException 토큰 헤더가 유효하지 않은 경우 예외 발생
* - 헤더가 null이거나 비어있는 경우
* - 헤더 형식이 "Bearer <token>" 형식이 아닌 경우
* - 토큰 타입이 Google ID 토큰인데 헤더 형식이 맞지 않는 경우
*/
private String extractToken(String authorizationHeader) {
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
return authorizationHeader.substring(7);
@Override
public String extractToken(String header, TokenType tokenType) {

if (header == null || header.isEmpty() || !header.startsWith("Bearer ")) {
CustomErrorCode errorCode = tokenType.equals(TokenType.GOOGLE)
? CustomErrorCode.INVALID_GOOGLE_TOKEN_HEADER
: CustomErrorCode.INVALID_AUTHORIZATION_HEADER;
throw new CustomException(errorCode);
}
throw new CustomException(CustomErrorCode.INVALID_AUTHORIZATION_HEADER);

return header.substring(7);
}

/**
* 구글 ID 토큰으로 로그인 처리
*
* @param authorizationHeader Authorization 헤더에 포함된 구글 ID 토큰
* @param idTokenHeader Google-ID-Token 헤더에 포함된 구글 ID 토큰
* @return JwtDTO를 포함하는 ResponseEntity
* @throws GeneralSecurityException 구글 ID 토큰 검증 중 발생하는 보안 예외
* @throws IOException IO 예외
* @throws CustomException 구글 ID 토큰이 유효하지 않거나 사용자가 등록되어 있지 않은 경우 예외
*/
@Override
@Transactional
public JwtDTO googleLogin(String authorizationHeader) throws GeneralSecurityException, IOException {
String idToken = extractToken(authorizationHeader);
public JwtDTO googleLogin(String idTokenHeader) throws GeneralSecurityException, IOException {

String idToken = extractToken(idTokenHeader, TokenType.GOOGLE);
GoogleUserInfoDTO userInfo = verifyGoogleIdToken(idToken);

if (!userRepository.existsByUsername(userInfo.getEmail())) {
Expand All @@ -132,7 +143,7 @@ public JwtDTO googleLogin(String authorizationHeader) throws GeneralSecurityExce
/**
* 구글 ID 토큰으로 회원가입 처리
*
* @param authorizationHeader Authorization 헤더에 포함된 구글 ID 토큰
* @param idTokenHeader Google-ID-Token 헤더에 포함된 구글 ID 토큰
* @param requestUserInfo GoogleJoinDTO 객체
* @return JwtDTO를 포함하는 ResponseEntity
* @throws GeneralSecurityException 구글 ID 토큰 검증 중 발생하는 보안 예외
Expand All @@ -141,8 +152,8 @@ public JwtDTO googleLogin(String authorizationHeader) throws GeneralSecurityExce
*/
@Override
@Transactional
public JwtDTO googleJoin(String authorizationHeader, GoogleJoinDTO requestUserInfo) throws GeneralSecurityException, IOException {
String idToken = extractToken(authorizationHeader);
public JwtDTO googleJoin(String idTokenHeader, GoogleJoinDTO requestUserInfo) throws GeneralSecurityException, IOException {
String idToken = extractToken(idTokenHeader, TokenType.GOOGLE);
GoogleUserInfoDTO userInfo = verifyGoogleIdToken(idToken);

if (userRepository.existsByUsername(userInfo.getEmail())) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
package com.tico.pomoro_do.global.base;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Getter;

@Getter
public class ErrorResponseDTO {
private int status;
private String name;
private int code;
private String code;
private String message;
// private final String data = ""; // 응답 데이터

@Builder
public ErrorResponseDTO(int status, String name, int code, String message) {
public ErrorResponseDTO(int status, String name, String code, String message) {
this.status = status;
this.name = name;
this.code = code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public class SuccessResponseDTO<T> {

@Schema(description = "상태 코드", nullable = false, example = "200")
private final int status; // 응답 코드 200번대
@Schema(description = "성공 코드", nullable = false, example = "Success")
private final String code = "SUCCESS";
@Schema(description = "상태 메세지", nullable = false, example = "성공하였습니다.")
private final String message; // 메시지
@Schema(description = "해당 API의 응답 데이터", nullable = false)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.tico.pomoro_do.global.common.enums;

public enum TokenType {

GOOGLE,

JWT
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public OpenAPI openAPI() {
// 인증 요청 방식에 HEADER 추가
SecurityScheme securityScheme = new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.scheme(BEARER_TOKEN_PREFIX)
.bearerFormat("JWT")
.in(SecurityScheme.In.HEADER)
.name("Authorization");
Expand Down
Loading

0 comments on commit 12a9cdc

Please sign in to comment.