diff --git a/backend/pomoro-do/src/main/java/com/tico/pomoro_do/domain/user/controller/AuthController.java b/backend/pomoro-do/src/main/java/com/tico/pomoro_do/domain/user/controller/AuthController.java index 85d8adfe..0d052ab1 100644 --- a/backend/pomoro-do/src/main/java/com/tico/pomoro_do/domain/user/controller/AuthController.java +++ b/backend/pomoro-do/src/main/java/com/tico/pomoro_do/domain/user/controller/AuthController.java @@ -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.*; @@ -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 = "구글 소셜 로그인을 통해 로그인을 수행합니다.
" - + "Authorization 헤더에는 구글 ID 토큰을 입력해야 합니다.
" + + "Google-ID-Token 헤더에는 구글 ID 토큰을 입력해야 합니다. 예시: Bearer 차돌이짱귀여워
" + "로그인 성공 시에는 JwtDTO를 포함하는 SuccessResponseDTO를 반환합니다.", parameters = @Parameter( - name = "Authorization", + name = "Google-ID-Token", description = "Google ID Token", in = ParameterIn.HEADER, required = true @@ -60,22 +62,23 @@ 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> googleLogin(@RequestHeader("Authorization") String authorizationHeader) { + public ResponseEntity> googleLogin(@RequestHeader("Google-ID-Token") String googleIdTokenHeader) { try { - JwtDTO jwtResponse = authService.googleLogin(authorizationHeader); + JwtDTO jwtResponse = authService.googleLogin(googleIdTokenHeader); SuccessResponseDTO response = SuccessResponseDTO.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); } } @@ -83,7 +86,7 @@ public ResponseEntity> googleLogin(@RequestHeader("Au /** * 구글 회원가입 API * - * @param authorizationHeader Authorization 헤더에는 구글 ID 토큰이 포함됩니다. + * @param googleIdTokenHeader Google-ID-Token 헤더에는 구글 ID 토큰이 포함됩니다. * @param requestUserInfo 회원가입 요청 정보가 포함된 DTO * @return 성공 시 JwtDTO를 포함하는 SuccessResponseDTO * @throws CustomException 구글 ID 토큰 검증에 실패한 경우 예외를 던집니다. @@ -91,10 +94,10 @@ public ResponseEntity> googleLogin(@RequestHeader("Au @Operation( summary = "회원 가입", description = "구글 소셜 로그인을 통해 회원가입을 수행합니다.
" - + "Authorization 헤더에는 구글 ID 토큰을 입력하고, 요청 본문에는 닉네임 등의 추가 정보를 포함해야 합니다.
" + + "Google-ID-Token 헤더에는 구글 ID 토큰을 입력하고, 요청 본문에는 닉네임 등의 추가 정보를 포함해야 합니다.
" + "회원가입 성공 시에는 JwtDTO를 포함하는 SuccessResponseDTO를 반환합니다.", parameters = @Parameter( - name = "Authorization", + name = "Google-ID-Token", description = "Google ID Token", in = ParameterIn.HEADER, required = true @@ -104,24 +107,25 @@ public ResponseEntity> 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> googleJoin(@RequestHeader("Authorization") String authorizationHeader, - @RequestBody GoogleJoinDTO requestUserInfo) { + public ResponseEntity> 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 response = SuccessResponseDTO.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); } } -} +} \ No newline at end of file diff --git a/backend/pomoro-do/src/main/java/com/tico/pomoro_do/domain/user/dto/request/GoogleJoinDTO.java b/backend/pomoro-do/src/main/java/com/tico/pomoro_do/domain/user/dto/request/GoogleJoinDTO.java index a70d873a..250dad40 100644 --- a/backend/pomoro-do/src/main/java/com/tico/pomoro_do/domain/user/dto/request/GoogleJoinDTO.java +++ b/backend/pomoro-do/src/main/java/com/tico/pomoro_do/domain/user/dto/request/GoogleJoinDTO.java @@ -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 { diff --git a/backend/pomoro-do/src/main/java/com/tico/pomoro_do/domain/user/service/AuthService.java b/backend/pomoro-do/src/main/java/com/tico/pomoro_do/domain/user/service/AuthService.java index deea2c19..e2718d99 100644 --- a/backend/pomoro-do/src/main/java/com/tico/pomoro_do/domain/user/service/AuthService.java +++ b/backend/pomoro-do/src/main/java/com/tico/pomoro_do/domain/user/service/AuthService.java @@ -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; @@ -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); diff --git a/backend/pomoro-do/src/main/java/com/tico/pomoro_do/domain/user/service/AuthServiceImpl.java b/backend/pomoro-do/src/main/java/com/tico/pomoro_do/domain/user/service/AuthServiceImpl.java index 2da5c1f3..9543c4ce 100644 --- a/backend/pomoro-do/src/main/java/com/tico/pomoro_do/domain/user/service/AuthServiceImpl.java +++ b/backend/pomoro-do/src/main/java/com/tico/pomoro_do/domain/user/service/AuthServiceImpl.java @@ -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; @@ -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() @@ -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); } } @@ -94,23 +94,33 @@ public JwtDTO createJwtTokens(String email, String role) { } /** - * Authorization 헤더에서 토큰 추출 + * 토큰 관련 헤더에서 토큰 값을 추출합니다. * - * @param authorizationHeader Authorization 헤더 - * @return 추출된 토큰 - * @throws CustomException Authorization 헤더가 유효하지 않은 경우 예외 + * @param header 토큰 헤더 (예: "Bearer ") + * @param tokenType 토큰의 타입 (Google ID 토큰 또는 JWT) + * @return 추출된 토큰 값 + * @throws CustomException 토큰 헤더가 유효하지 않은 경우 예외 발생 + * - 헤더가 null이거나 비어있는 경우 + * - 헤더 형식이 "Bearer " 형식이 아닌 경우 + * - 토큰 타입이 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 예외 @@ -118,8 +128,9 @@ private String extractToken(String authorizationHeader) { */ @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())) { @@ -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 토큰 검증 중 발생하는 보안 예외 @@ -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())) { diff --git a/backend/pomoro-do/src/main/java/com/tico/pomoro_do/global/base/ErrorResponseDTO.java b/backend/pomoro-do/src/main/java/com/tico/pomoro_do/global/base/ErrorResponseDTO.java index 13919327..8f4ce27d 100644 --- a/backend/pomoro-do/src/main/java/com/tico/pomoro_do/global/base/ErrorResponseDTO.java +++ b/backend/pomoro-do/src/main/java/com/tico/pomoro_do/global/base/ErrorResponseDTO.java @@ -1,5 +1,6 @@ package com.tico.pomoro_do.global.base; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; import lombok.Getter; @@ -7,11 +8,12 @@ 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; diff --git a/backend/pomoro-do/src/main/java/com/tico/pomoro_do/global/base/SuccessResponseDTO.java b/backend/pomoro-do/src/main/java/com/tico/pomoro_do/global/base/SuccessResponseDTO.java index ff78e92c..6befbe3d 100644 --- a/backend/pomoro-do/src/main/java/com/tico/pomoro_do/global/base/SuccessResponseDTO.java +++ b/backend/pomoro-do/src/main/java/com/tico/pomoro_do/global/base/SuccessResponseDTO.java @@ -10,6 +10,8 @@ public class SuccessResponseDTO { @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) diff --git a/backend/pomoro-do/src/main/java/com/tico/pomoro_do/global/common/enums/TokenType.java b/backend/pomoro-do/src/main/java/com/tico/pomoro_do/global/common/enums/TokenType.java new file mode 100644 index 00000000..a1e51e90 --- /dev/null +++ b/backend/pomoro-do/src/main/java/com/tico/pomoro_do/global/common/enums/TokenType.java @@ -0,0 +1,8 @@ +package com.tico.pomoro_do.global.common.enums; + +public enum TokenType { + + GOOGLE, + + JWT +} diff --git a/backend/pomoro-do/src/main/java/com/tico/pomoro_do/global/config/SwaggerConfig.java b/backend/pomoro-do/src/main/java/com/tico/pomoro_do/global/config/SwaggerConfig.java index 826ca8e7..e4a55ed0 100644 --- a/backend/pomoro-do/src/main/java/com/tico/pomoro_do/global/config/SwaggerConfig.java +++ b/backend/pomoro-do/src/main/java/com/tico/pomoro_do/global/config/SwaggerConfig.java @@ -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"); diff --git a/backend/pomoro-do/src/main/java/com/tico/pomoro_do/global/exception/CustomErrorCode.java b/backend/pomoro-do/src/main/java/com/tico/pomoro_do/global/exception/CustomErrorCode.java index 62d0f93e..cf2638db 100644 --- a/backend/pomoro-do/src/main/java/com/tico/pomoro_do/global/exception/CustomErrorCode.java +++ b/backend/pomoro-do/src/main/java/com/tico/pomoro_do/global/exception/CustomErrorCode.java @@ -16,36 +16,48 @@ public enum CustomErrorCode { /* 409 CONFLICT : Resource의 현재 상태와 충돌. 보통 중복된 데이터 존재 */ //유저 관련 에러: -100번대 - EMAIL_EXIST(HttpStatus.BAD_REQUEST, -100, "이미 사용 중인 이메일입니다."), - NICKNAME_NULL(HttpStatus.BAD_REQUEST, -101, "닉네임을 입력해주세요."), - USER_NOT_FOUND(HttpStatus.NOT_FOUND, -102, "가입된 사용자가 아닙니다."), - PROFILE_UPLOAD_FAILED(HttpStatus.FORBIDDEN, -103, "프로필 이미지 변경에 실패했습니다."), - USER_ALREADY_REGISTERED(HttpStatus.CONFLICT, -104, "이미 등록된 사용자입니다."), + EMAIL_EXIST(HttpStatus.BAD_REQUEST, "U-100", "이미 사용 중인 이메일입니다."), + NICKNAME_NULL(HttpStatus.BAD_REQUEST, "U-101", "닉네임을 입력해주세요."), + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "U-102", "가입된 사용자가 아닙니다."), + PROFILE_UPLOAD_FAILED(HttpStatus.FORBIDDEN, "U-103", "프로필 이미지 변경에 실패했습니다."), + USER_ALREADY_REGISTERED(HttpStatus.CONFLICT, "U-104", "이미 등록된 사용자입니다."), //Token 관련 에러 -200번대 - ACCESS_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, -201, "액세스 토큰이 만료되었습니다."), - REFRESH_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, -202, "리프레시 토큰이 만료되었습니다."), - INVALID_ACCESS_TOKEN(HttpStatus.FORBIDDEN, -203, "액세스 토큰이 유효하지 않습니다."), - INVALID_REFRESH_TOKEN(HttpStatus.FORBIDDEN, -204, "리프레시 토큰이 유효하지 않습니다."), - MISSING_ACCESS_TOKEN(HttpStatus.BAD_REQUEST, -205, "액세스 토큰이 없습니다."), - MISSING_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, -206, "리프레시 토큰이 없습니다."), - MISSING_REFRESH_TOKEN_IN_DB(HttpStatus.BAD_REQUEST, -207, "DB에 리프레시 토큰이 없습니다."), - REFRESH_TOKEN_MISMATCH(HttpStatus.BAD_REQUEST, -208, "데이터베이스의 리프레시 토큰과 일치하지 않습니다."), - INVALID_AUTHORIZATION_HEADER(HttpStatus.BAD_REQUEST, -209, "AUTHORIZATION 헤더의 토큰이 유효하지 않습니다."), - UNAUTHORIZED_ACCESS(HttpStatus.UNAUTHORIZED, -210, "인증되지 않은 접근입니다."), + ACCESS_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "T-201", "액세스 토큰이 만료되었습니다."), + REFRESH_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "T-202", "리프레시 토큰이 만료되었습니다."), + INVALID_ACCESS_TOKEN(HttpStatus.FORBIDDEN, "T-203", "액세스 토큰이 유효하지 않습니다."), + INVALID_REFRESH_TOKEN(HttpStatus.FORBIDDEN, "T-204", "리프레시 토큰이 유효하지 않습니다."), + MISSING_ACCESS_TOKEN(HttpStatus.BAD_REQUEST, "T-205", "액세스 토큰이 없습니다."), + MISSING_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, "T-206", "리프레시 토큰이 없습니다."), + MISSING_REFRESH_TOKEN_IN_DB(HttpStatus.BAD_REQUEST, "T-207", "DB에 리프레시 토큰이 없습니다."), + REFRESH_TOKEN_MISMATCH(HttpStatus.BAD_REQUEST, "T-208", "데이터베이스의 리프레시 토큰과 일치하지 않습니다."), + INVALID_AUTHORIZATION_HEADER(HttpStatus.BAD_REQUEST, "T-209", "AUTHORIZATION 헤더의 토큰이 유효하지 않습니다."), + UNAUTHORIZED_ACCESS(HttpStatus.UNAUTHORIZED, "T-210", "인증되지 않은 접근입니다."), //구글 토큰 관련 에러: -300번대 - GOOGLE_TOKEN_VERIFICATION_FAILED(HttpStatus.UNAUTHORIZED, -300, "구글 ID 토큰 검증에 실패했습니다."), - GOOGLE_TOKEN_INVALID(HttpStatus.UNAUTHORIZED, -301, "구글 ID 토큰이 유효하지 않습니다."), + GOOGLE_TOKEN_VERIFICATION_FAILED(HttpStatus.UNAUTHORIZED, "G-300", "구글 ID 토큰 검증에 실패했습니다."), + INVALID_GOOGLE_TOKEN_HEADER(HttpStatus.BAD_REQUEST, "G-301", "GOOGLE_ID_TOKEN 헤더의 토큰이 유효하지 않습니다."), //관리자 관련 에러: -400번대 - NOT_AN_ADMIN(HttpStatus.FORBIDDEN, -400, "관리자 권한이 없습니다."), - INVALID_ADMIN_EMAIL(HttpStatus.BAD_REQUEST, -401, "허용되지 않은 관리자 이메일입니다."), - ADMIN_EMAIL_ONLY(HttpStatus.FORBIDDEN, -402, "관리자 이메일만 접근할 수 있습니다."), - ADMIN_LOGIN_FAILED(HttpStatus.BAD_REQUEST, -403, "관리자 정보가 일치하지 않습니다. 관리자 로그인에 실패했습니다."); + NOT_AN_ADMIN(HttpStatus.FORBIDDEN, "A-400", "관리자 권한이 없습니다."), + INVALID_ADMIN_EMAIL(HttpStatus.BAD_REQUEST, "A-401", "허용되지 않은 관리자 이메일입니다."), + ADMIN_EMAIL_ONLY(HttpStatus.FORBIDDEN, "A-402", "관리자 이메일만 접근할 수 있습니다."), + ADMIN_LOGIN_FAILED(HttpStatus.BAD_REQUEST, "A-403", "관리자 정보가 일치하지 않습니다. 관리자 로그인에 실패했습니다."), + + //Validation 관련 에러: -500번대 + VALIDATION_FAILED(HttpStatus.BAD_REQUEST, "V-500", "유효성 검증에 실패했습니다."), + MISSING_REQUEST_BODY(HttpStatus.BAD_REQUEST, "V-501", "요청 본문이 누락되었습니다."), + MISSING_REQUEST_HEADER(HttpStatus.BAD_REQUEST, "V-502", "요청 헤더가 누락되었습니다."), + + //서버 내부 관련 에러: -900번대 + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "S-900", "서버 내부 오류가 발생했습니다."), + RESOURCE_NOT_FOUND(HttpStatus.NOT_FOUND, "S-901", "요청한 자원을 찾을 수 없습니다."), + INVALID_ARGUMENT(HttpStatus.BAD_REQUEST, "S-902", "유효하지 않은 인수입니다."), + ACCESS_DENIED(HttpStatus.FORBIDDEN, "S-903", "접근이 거부되었습니다."); + private final HttpStatus httpStatus; // HttpStatus - private final int code; // -100 + private final String code; // U-100 private final String message; // 설명 } diff --git a/backend/pomoro-do/src/main/java/com/tico/pomoro_do/global/exception/ErrorResponseEntity.java b/backend/pomoro-do/src/main/java/com/tico/pomoro_do/global/exception/ErrorResponseEntity.java index 632f3d61..647a2ce0 100644 --- a/backend/pomoro-do/src/main/java/com/tico/pomoro_do/global/exception/ErrorResponseEntity.java +++ b/backend/pomoro-do/src/main/java/com/tico/pomoro_do/global/exception/ErrorResponseEntity.java @@ -10,14 +10,17 @@ @Schema(description = "Error Response") public class ErrorResponseEntity { //Custom Error 내용을 담을 Response Entity를 생성한다. + @Schema(description = "HTTP 상태 코드") private int status; @Schema(description = "에러 이름") private String name; @Schema(description = "커스텀 에러 코드") - private int code; + private String code; @Schema(description = "에러 메시지") private String message; +// @Schema(description = "응답 데이터: 빈값") +// private final String data = ""; // 응답 데이터 public static ResponseEntity toResponseEntity(CustomErrorCode e){ return ResponseEntity @@ -35,7 +38,7 @@ public static ResponseEntity toResponseEntity(CustomErrorCo //{ // "status": 404, // "name": "USER_NOT_FOUND", - // "code": -1000, + // "code": "USER-100“, // "message": "사용자를 찾을 수 없습니다." //} diff --git a/backend/pomoro-do/src/main/java/com/tico/pomoro_do/global/exception/GlobalExceptionHandler.java b/backend/pomoro-do/src/main/java/com/tico/pomoro_do/global/exception/GlobalExceptionHandler.java index 0a7c5920..4b87413f 100644 --- a/backend/pomoro-do/src/main/java/com/tico/pomoro_do/global/exception/GlobalExceptionHandler.java +++ b/backend/pomoro-do/src/main/java/com/tico/pomoro_do/global/exception/GlobalExceptionHandler.java @@ -1,17 +1,134 @@ package com.tico.pomoro_do.global.exception; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingRequestHeaderException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; + +import java.util.HashMap; +import java.util.Map; +import java.util.NoSuchElementException; @ControllerAdvice // 모든 @Controller 즉, 전역에서 발생할 수 있는 예외를 잡아 처리한다. public class GlobalExceptionHandler { //Controller 전역에서 발생하는 Custom Error를 잡아줄 Handler를 생성한다. + + // CustomException 처리 @ExceptionHandler(CustomException.class) //발생한 CustomException 예외를 잡아서 하나의 메소드에서 공통 처리한다. protected ResponseEntity handleCustomException(CustomException e) { return ErrorResponseEntity.toResponseEntity(e.getErrorCode()); } + // Validation 오류 처리 + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity handleValidationExceptions(MethodArgumentNotValidException ex) { + Map errors = new HashMap<>(); + for (FieldError error : ex.getBindingResult().getFieldErrors()) { + errors.put(error.getField(), error.getDefaultMessage()); + } + + ErrorResponseEntity errorResponse = ErrorResponseEntity.builder() + .status(HttpStatus.BAD_REQUEST.value()) + .name("Validation Error") + .code(CustomErrorCode.VALIDATION_FAILED.getCode()) + .message(errors.toString()) + .build(); + + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + // 요청 본문이 잘못된 경우 처리 + @ExceptionHandler(HttpMessageNotReadableException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity handleHttpMessageNotReadableException(HttpMessageNotReadableException ex) { + ErrorResponseEntity errorResponse = ErrorResponseEntity.builder() + .status(HttpStatus.BAD_REQUEST.value()) + .name("Bad Request") + .code(CustomErrorCode.MISSING_REQUEST_BODY.getCode()) + .message("Required request body is missing.") + .build(); + + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + + // 요청 헤더가 누락된 경우 처리 + @ExceptionHandler(MissingRequestHeaderException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity handleMissingRequestHeaderException(MissingRequestHeaderException ex) { + ErrorResponseEntity errorResponse = ErrorResponseEntity.builder() + .status(HttpStatus.BAD_REQUEST.value()) + .name("Bad Request") + .code(CustomErrorCode.MISSING_REQUEST_HEADER.getCode()) + .message("Required request header is missing: " + ex.getHeaderName()) + .build(); + + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + // ElementNotFound 예외 처리 + @ExceptionHandler(NoSuchElementException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ResponseEntity handleNoSuchElementException(NoSuchElementException ex) { + ErrorResponseEntity errorResponse = ErrorResponseEntity.builder() + .status(HttpStatus.NOT_FOUND.value()) + .name("Not Found") + .code(CustomErrorCode.RESOURCE_NOT_FOUND.getCode()) + .message("The requested resource was not found.") + .build(); + + return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); + } + + // IllegalArgumentException 처리 + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity handleIllegalArgumentException(IllegalArgumentException ex) { + ErrorResponseEntity errorResponse = ErrorResponseEntity.builder() + .status(HttpStatus.BAD_REQUEST.value()) + .name("Bad Request") + .code(CustomErrorCode.INVALID_ARGUMENT.getCode()) + .message(ex.getMessage()) + .build(); + + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + // 권한이 없는 접근 시 처리 + @ExceptionHandler(AccessDeniedException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public ResponseEntity handleAccessDeniedException(AccessDeniedException ex) { + ErrorResponseEntity errorResponse = ErrorResponseEntity.builder() + .status(HttpStatus.FORBIDDEN.value()) + .name("Forbidden") + .code(CustomErrorCode.ACCESS_DENIED.getCode()) + .message("Access to the resource is denied.") + .build(); + + return new ResponseEntity<>(errorResponse, HttpStatus.FORBIDDEN); + } + + // 기타 모든 예외 처리 + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ResponseEntity handleException(Exception ex) { + ErrorResponseEntity errorResponse = ErrorResponseEntity.builder() + .status(HttpStatus.INTERNAL_SERVER_ERROR.value()) + .name("Internal Server Error") + .code(CustomErrorCode.INTERNAL_SERVER_ERROR.getCode()) + .message("An unexpected error occurred: " + ex.getMessage()) + .build(); + + return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } + //@ControllerAdvice + @ExceptionHandler //모든 컨트롤러에서 발생하는 CustomException을 catch한다. } \ No newline at end of file