From 875cc3301813a73dfe147dc11ab074678b60a955 Mon Sep 17 00:00:00 2001 From: can019 Date: Sat, 13 Sep 2025 12:51:29 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EA=B8=B0=EB=B3=B8=EC=A0=81?= =?UTF-8?q?=EC=9D=B8=20global=20exception=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/security/SecurityConfig.java | 11 ++++++ .../exception/GlobalExceptionHandler.java | 33 ++++++++++++++++++ .../exception/RestAccessDeniedHandler.java | 33 ++++++++++++++++++ .../RestAuthenticationEntryPoint.java | 34 +++++++++++++++++++ 4 files changed, 111 insertions(+) create mode 100644 apps/user-service/src/main/java/site/icebang/global/handler/exception/GlobalExceptionHandler.java create mode 100644 apps/user-service/src/main/java/site/icebang/global/handler/exception/RestAccessDeniedHandler.java create mode 100644 apps/user-service/src/main/java/site/icebang/global/handler/exception/RestAuthenticationEntryPoint.java diff --git a/apps/user-service/src/main/java/site/icebang/global/config/security/SecurityConfig.java b/apps/user-service/src/main/java/site/icebang/global/config/security/SecurityConfig.java index aba3ee3c..c915867d 100644 --- a/apps/user-service/src/main/java/site/icebang/global/config/security/SecurityConfig.java +++ b/apps/user-service/src/main/java/site/icebang/global/config/security/SecurityConfig.java @@ -19,16 +19,23 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; +import com.fasterxml.jackson.databind.ObjectMapper; + import lombok.RequiredArgsConstructor; import site.icebang.domain.auth.service.AuthCredentialAdapter; import site.icebang.global.config.security.endpoints.SecurityEndpoints; +import site.icebang.global.handler.exception.RestAccessDeniedHandler; +import site.icebang.global.handler.exception.RestAuthenticationEntryPoint; @Configuration @RequiredArgsConstructor public class SecurityConfig { private final Environment environment; private final AuthCredentialAdapter userDetailsService; + private final ObjectMapper objectMapper; + private final RestAuthenticationEntryPoint restAuthenticationEntryPoint; + private final RestAccessDeniedHandler restAccessDeniedHandler; @Bean public AuthenticationProvider authenticationProvider() { @@ -97,6 +104,10 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .logout( logout -> logout.logoutUrl("/auth/logout").logoutSuccessUrl("/auth/login").permitAll()) .csrf(AbstractHttpConfigurer::disable) + .exceptionHandling( + ex -> + ex.authenticationEntryPoint(restAuthenticationEntryPoint) + .accessDeniedHandler(restAccessDeniedHandler)) .build(); } diff --git a/apps/user-service/src/main/java/site/icebang/global/handler/exception/GlobalExceptionHandler.java b/apps/user-service/src/main/java/site/icebang/global/handler/exception/GlobalExceptionHandler.java new file mode 100644 index 00000000..a5296697 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/global/handler/exception/GlobalExceptionHandler.java @@ -0,0 +1,33 @@ +package site.icebang.global.handler.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.resource.NoResourceFoundException; + +import site.icebang.common.dto.ApiResponse; + +@RestControllerAdvice +public class GlobalExceptionHandler { + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse handleValidation(MethodArgumentNotValidException ex) { + String detail = ex.getBindingResult().toString(); + return ApiResponse.error("Validation failed: " + detail, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ApiResponse handleGeneric(Exception ex) { + return ApiResponse.error( + "Internal error: " + ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); + } + + @ExceptionHandler(NoResourceFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ApiResponse handleNotFound(NoResourceFoundException ex) { + return ApiResponse.error("Notfound: " + ex.getMessage(), HttpStatus.NOT_FOUND); + } +} diff --git a/apps/user-service/src/main/java/site/icebang/global/handler/exception/RestAccessDeniedHandler.java b/apps/user-service/src/main/java/site/icebang/global/handler/exception/RestAccessDeniedHandler.java new file mode 100644 index 00000000..efeffde1 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/global/handler/exception/RestAccessDeniedHandler.java @@ -0,0 +1,33 @@ +package site.icebang.global.handler.exception; + +import java.io.IOException; + +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; + +import site.icebang.common.dto.ApiResponse; + +@Component +@RequiredArgsConstructor +public class RestAccessDeniedHandler implements AccessDeniedHandler { + private final ObjectMapper objectMapper; + + @Override + public void handle( + HttpServletRequest request, HttpServletResponse response, AccessDeniedException ex) + throws IOException { + ApiResponse body = ApiResponse.error("Access denied", HttpStatus.FORBIDDEN); + + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write(objectMapper.writeValueAsString(body)); + } +} diff --git a/apps/user-service/src/main/java/site/icebang/global/handler/exception/RestAuthenticationEntryPoint.java b/apps/user-service/src/main/java/site/icebang/global/handler/exception/RestAuthenticationEntryPoint.java new file mode 100644 index 00000000..b7c50d76 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/global/handler/exception/RestAuthenticationEntryPoint.java @@ -0,0 +1,34 @@ +package site.icebang.global.handler.exception; + +import java.io.IOException; + +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; + +import site.icebang.common.dto.ApiResponse; + +@Component +@RequiredArgsConstructor +public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { + private final ObjectMapper objectMapper; + + @Override + public void commence( + HttpServletRequest request, HttpServletResponse response, AuthenticationException ex) + throws IOException { + ApiResponse body = + ApiResponse.error("Authentication required", HttpStatus.UNAUTHORIZED); + + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write(objectMapper.writeValueAsString(body)); + } +} From 217ec4fdadbc7e38fc04faae990750e68d7e2f35 Mon Sep 17 00:00:00 2001 From: can019 Date: Sat, 13 Sep 2025 12:59:08 +0900 Subject: [PATCH 2/3] chore: Security exception handling --- .../handler/exception/GlobalExceptionHandler.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/apps/user-service/src/main/java/site/icebang/global/handler/exception/GlobalExceptionHandler.java b/apps/user-service/src/main/java/site/icebang/global/handler/exception/GlobalExceptionHandler.java index a5296697..f6d76025 100644 --- a/apps/user-service/src/main/java/site/icebang/global/handler/exception/GlobalExceptionHandler.java +++ b/apps/user-service/src/main/java/site/icebang/global/handler/exception/GlobalExceptionHandler.java @@ -1,6 +1,8 @@ package site.icebang.global.handler.exception; import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.AuthenticationException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; @@ -30,4 +32,16 @@ public ApiResponse handleGeneric(Exception ex) { public ApiResponse handleNotFound(NoResourceFoundException ex) { return ApiResponse.error("Notfound: " + ex.getMessage(), HttpStatus.NOT_FOUND); } + + @ExceptionHandler(AuthenticationException.class) + @ResponseStatus(HttpStatus.UNAUTHORIZED) + public ApiResponse handleAuthentication(AuthenticationException ex) { + return ApiResponse.error("Authentication failed: " + ex.getMessage(), HttpStatus.UNAUTHORIZED); + } + + @ExceptionHandler(AccessDeniedException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public ApiResponse handleAccessDenied(AccessDeniedException ex) { + return ApiResponse.error("Access denied: " + ex.getMessage(), HttpStatus.FORBIDDEN); + } } From a7a858d505ccb444a3b6c23810f4d7ff7e086a3f Mon Sep 17 00:00:00 2001 From: can019 Date: Sat, 13 Sep 2025 13:05:21 +0900 Subject: [PATCH 3/3] feat: Duplicate Exception handling --- .../exception/DuplicateDataException.java | 25 ++++++++++ .../domain/auth/service/AuthService.java | 3 +- .../exception/GlobalExceptionHandler.java | 7 +++ .../scenario/UserRegistrationFlowE2eTest.java | 49 +------------------ 4 files changed, 35 insertions(+), 49 deletions(-) create mode 100644 apps/user-service/src/main/java/site/icebang/common/exception/DuplicateDataException.java diff --git a/apps/user-service/src/main/java/site/icebang/common/exception/DuplicateDataException.java b/apps/user-service/src/main/java/site/icebang/common/exception/DuplicateDataException.java new file mode 100644 index 00000000..e673ab86 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/common/exception/DuplicateDataException.java @@ -0,0 +1,25 @@ +package site.icebang.common.exception; + +public class DuplicateDataException extends RuntimeException { + + public DuplicateDataException() { + super(); + } + + public DuplicateDataException(String message) { + super(message); + } + + public DuplicateDataException(String message, Throwable cause) { + super(message, cause); + } + + public DuplicateDataException(Throwable cause) { + super(cause); + } + + protected DuplicateDataException( + String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/auth/service/AuthService.java b/apps/user-service/src/main/java/site/icebang/domain/auth/service/AuthService.java index 091861b2..25a5bd42 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/auth/service/AuthService.java +++ b/apps/user-service/src/main/java/site/icebang/domain/auth/service/AuthService.java @@ -6,6 +6,7 @@ import lombok.RequiredArgsConstructor; +import site.icebang.common.exception.DuplicateDataException; import site.icebang.common.utils.RandomPasswordGenerator; import site.icebang.domain.auth.dto.RegisterDto; import site.icebang.domain.auth.mapper.AuthMapper; @@ -23,7 +24,7 @@ public class AuthService { public void registerUser(RegisterDto registerDto) { if (authMapper.existsByEmail(registerDto.getEmail())) { - throw new IllegalArgumentException("이미 가입된 이메일입니다."); + throw new DuplicateDataException("이미 가입된 이메일입니다."); } String randomPassword = passwordGenerator.generate(); String hashedPassword = passwordEncoder.encode(randomPassword); diff --git a/apps/user-service/src/main/java/site/icebang/global/handler/exception/GlobalExceptionHandler.java b/apps/user-service/src/main/java/site/icebang/global/handler/exception/GlobalExceptionHandler.java index f6d76025..6923f455 100644 --- a/apps/user-service/src/main/java/site/icebang/global/handler/exception/GlobalExceptionHandler.java +++ b/apps/user-service/src/main/java/site/icebang/global/handler/exception/GlobalExceptionHandler.java @@ -10,6 +10,7 @@ import org.springframework.web.servlet.resource.NoResourceFoundException; import site.icebang.common.dto.ApiResponse; +import site.icebang.common.exception.DuplicateDataException; @RestControllerAdvice public class GlobalExceptionHandler { @@ -44,4 +45,10 @@ public ApiResponse handleAuthentication(AuthenticationException ex) { public ApiResponse handleAccessDenied(AccessDeniedException ex) { return ApiResponse.error("Access denied: " + ex.getMessage(), HttpStatus.FORBIDDEN); } + + @ExceptionHandler(DuplicateDataException.class) + @ResponseStatus(HttpStatus.CONFLICT) + public ApiResponse handleDuplicateData(DuplicateDataException ex) { + return ApiResponse.error("Duplicate: " + ex.getMessage(), HttpStatus.CONFLICT); + } } diff --git a/apps/user-service/src/test/java/site/icebang/e2e/scenario/UserRegistrationFlowE2eTest.java b/apps/user-service/src/test/java/site/icebang/e2e/scenario/UserRegistrationFlowE2eTest.java index a873d2d5..1cf10e95 100644 --- a/apps/user-service/src/test/java/site/icebang/e2e/scenario/UserRegistrationFlowE2eTest.java +++ b/apps/user-service/src/test/java/site/icebang/e2e/scenario/UserRegistrationFlowE2eTest.java @@ -7,7 +7,6 @@ import java.util.HashMap; import java.util.Map; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.http.*; @@ -116,52 +115,6 @@ void completeUserRegistrationFlow() throws Exception { logCompletion("ERP 사용자 등록 플로우"); } - @Disabled - @DisplayName("로그인 없이 리소스 접근 시 모든 요청 차단") - void accessResourcesWithoutLogin_shouldFailForAll() { - logStep(1, "인증 없이 조직 목록 조회 시도"); - - // 1. 로그인 없이 조직 목록 조회 시도 - ResponseEntity orgResponse = - restTemplate.getForEntity(getV0ApiUrl("/organizations"), Map.class); - - assertThat(orgResponse.getStatusCode()).isIn(HttpStatus.UNAUTHORIZED, HttpStatus.FORBIDDEN); - logSuccess("미인증 조직 조회 차단 확인"); - - logStep(2, "인증 없이 조직 옵션 조회 시도"); - - // 2. 로그인 없이 조직 옵션 조회 시도 - ResponseEntity optResponse = - restTemplate.getForEntity(getV0ApiUrl("/organizations/1/options"), Map.class); - - assertThat(optResponse.getStatusCode()).isIn(HttpStatus.UNAUTHORIZED, HttpStatus.FORBIDDEN); - logSuccess("미인증 옵션 조회 차단 확인"); - - logStep(3, "인증 없이 회원가입 시도"); - - // 3. 로그인 없이 회원가입 시도 - Map registerRequest = new HashMap<>(); - registerRequest.put("name", "테스트사용자"); - registerRequest.put("email", "test@example.com"); - registerRequest.put("orgId", 1); - registerRequest.put("deptId", 2); - registerRequest.put("positionId", 5); - registerRequest.put("roleIds", Arrays.asList(6)); - - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - - HttpEntity> entity = new HttpEntity<>(registerRequest, headers); - - ResponseEntity regResponse = - restTemplate.postForEntity(getV0ApiUrl("/auth/register"), entity, Map.class); - - assertThat(regResponse.getStatusCode()).isIn(HttpStatus.UNAUTHORIZED, HttpStatus.FORBIDDEN); - logSuccess("미인증 회원가입 차단 확인"); - - logCompletion("ERP 보안 검증"); - } - @Test @DisplayName("잘못된 자격증명으로 로그인 시도 시 실패") void loginWithInvalidCredentials_shouldFail() { @@ -200,7 +153,7 @@ void loginWithInvalidCredentials_shouldFail() { } @SuppressWarnings("unchecked") - @Disabled + @Test @DisplayName("중복 이메일로 사용자 등록 시도 시 실패") void register_withDuplicateEmail_shouldFail() { // 선행 조건: 관리자 로그인