Search

Filter 에 따른 Exception Custom - EntryPoint 란?

태그
Exception
EntryPoint
spring security
분류
Spring Boot

왜 예외 처리를 해야하나요?

똑같은 400401403 상태여도 원인은 다를 수 있습니다. 어디서 어떻게 발생한 오류인지 알아야 front 와 backend 둘 다 문제를 빠르게 해결할 수 있죠. 그래서 저는 특정 상황에서 발생 할 수 있는 예외를 아래와 같이 custom 하기로 했습니다.
{ code:"에러와 관련한 코드" }
JSON
복사
코드에 관한 설명은 문서화를 하여 따로 관리하기로 하였습니다.

예외는 어디서 발생하는가?

예외 처리를 하기 전, 이 예외는 어디서 발생하는지 먼저 알아야 합니다. 이는 필터의 순서를 고려해야 하죠. 제가 구현한 서비스의 경우, 필터의 순서는 아래와 같습니다.
2024-02-18 19:57:14 2024-02-18T10:57:14.098Z INFO 1 --- [main] o.s.s.web.DefaultSecurityFilterChain: Will secure any request with [ org.springframework.security.web.session.DisableEncodeUrlFilter@60f70c9e, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@6db64abf, org.springframework.security.web.context.SecurityContextHolderFilter@6108c962, org.springframework.security.web.header.HeaderWriterFilter@28d74041, org.springframework.web.filter.CorsFilter@39d37da8, org.springframework.security.web.authentication.logout.LogoutFilter@75c0e6be, com.starbucks.backend.global.security.CustomAuthenticationFilter@6f53f5a4, com.starbucks.backend.global.jwt.JWTAuthenticationFilter@21ebf9be, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@64455184, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@6f749a1f, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@34714012, org.springframework.security.web.session.SessionManagementFilter@5cbf9aee, org.springframework.security.web.access.ExceptionTranslationFilter@721f3526, org.springframework.security.web.access.intercept.AuthorizationFilter@4dd28982 ]
Plain Text
복사
중요한 필터만 자세히 보면, 아래와 같은 순서로 되어 있습니다.
1. SecurityContextHolderFilter
2. CorsFilter
3. CustomAuthenticationFilter
4. JWTAuthenticationFilter
5. SecurityContextHolderAwareRequestFilter
6. ExceptionTranslationFilter
7. AuthorizationFilter
오류가 7 번까지 다 거치고 나서 발생 하는지, 아니면 그 전에 발생하는지에 따라 처리하는 방법이 달라집니다.

인증 필터 이후의 Exception Custom

예외를 다룰 핸들러 작성하기

클래스를 만들고, 위에 @ControllerAdvice 를 달아주세요. 이는 전역적으로 컨트롤러에서 발생하는 예외를 처리하기 위해 사용됩니다.
@ControllerAdvice public class GlobalUserExceptionHandler { }
Java
복사

발생시킨 예외 커스텀 하기

서비스 단에서 예외를 발생시키고, 해당 예외를 잡아서 처리하면 됩니다. 이때 @ExceptionHandler 를 달아주세요. 이는 특정 예외가 발생했을 때 실행될 메소드를 지정합니다.
@ControllerAdvice public class GlobalUserExceptionHandler { @ExceptionHandler(EntityNotFoundException.class) public ResponseEntity<?> handleEntityNotFoundException() { ErrorResponse errorResponse = new ErrorResponse("해당 유저를 찾을 수 없습니다.", "USER_02"); return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); } ... }
Java
복사

ErrorResponse.java

Custom 한 예외를 front 에 반환하기 위해 DTO 를 만들어 줍니다.
public record ErrorResponse(String message, String errorCode) { }
Java
복사

인증 필터를 거치는 중 발생한 Exception Custom

위의 인증 필터를 거치는 중 발생한 에러, AuthenticationException 의 경우 위와 같이 만들면 custom 한 에러가 나타나지 않습니다. 그렇다면 어떻게 작성해야 할까요?

AuthenticationException

먼저 AuthenticationException 의 종류는 아래와 같이 있습니다.
1.
BadCredentialsException: 제공된 인증 정보가 유효하지 않을 때 발생합니다.
2.
UsernameNotFoundException: 인증을 시도하는 사용자의 이름을 데이터베이스에서 찾을 수 없을 때 발생 합니다.
3.
AccountExpiredException: 사용자의 계정이 만료되었을 때 발생합니다. 계정의 사용 기간이 정해져 있고, 그 기간이 지난 경우에 발생 합니다.
4.
CredentialsExpiredException: 사용자의 인증 정보가 만료되었을 때 발생합니다. 정기적인 비밀번호 변경 정책에 의해 비밀번호가 만료된 경우에 이 예외가 발생할 수 있습니다.
5.
DisabledException: 사용자 계정이 비활성화 상태일 때 발생합니다. 관리자에 의해 계정이 비활성화되었거나, 사용자가 일정 기간 동안 로그인하지 않아 자동으로 비활성화된 경우에 이 예외가 발생할 수 있습니다.
6.
LockedException: 사용자 계정이 잠겨 있을 때 발생합니다. 여러 번의 잘못된 로그인 시도 후 계정이 잠기는 보안 정책에 의해 이 예외가 발생할 수 있습니다.
7.
InsufficientAuthenticationException: 요청이 처리되기 위해 필요한 인증 수준을 충족하지 못했을 때 발생합니다.
8.
AuthenticationServiceException: 인증 서비스 자체에서 일반적인 오류가 발생했을 때 사용됩니다.

CustomAuthenticationEntryPoint.java

아래와 같이 AuthenticationException 을 상속 받은 클래스를 만들고, 발생시킨 예외를 처리하면 됩니다. 프론트에 해당 오류를 보내기 위해 setResponse 메서드를 작성하는 것 또한 잊지 마세요.
package com.planner.travel.global.security; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import java.io.IOException; @Component @RequiredArgsConstructor public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { if (authException instanceof UsernameNotFoundException) { response.setStatus(HttpStatus.NOT_FOUND.value()); setResponse(response, "AUTH_01"); } else if (authException instanceof BadCredentialsException) { response.setStatus(HttpStatus.BAD_REQUEST.value()); setResponse(response, "AUTH_02"); } else if (authException instanceof InsufficientAuthenticationException) { response.setStatus(HttpStatus.UNAUTHORIZED.value()); setResponse(response, "AUTH_03"); } } private void setResponse(HttpServletResponse response, String errorCode) throws IOException { response.setContentType("application/json;charset=UTF-8"); response.getWriter().println( "{\"errorCode\" : \"" + errorCode + "\"}" ); } }
Java
복사

Custom 한 EntryPoint 등록하기

이렇게 작성한 entryporint 는 securityConfiguration 에 등록해줘야 합니다.
@Bean public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { httpSecurity .csrf(AbstractHttpConfigurer::disable) .cors(cors -> {}) .authorizeHttpRequests((authorizeRequest) -> authorizeRequest .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() .requestMatchers("/api/v1/auth/signup").permitAll() .requestMatchers("/api/v1/auth/login").permitAll() .requestMatchers("/api/v1/auth/token/**").permitAll() .requestMatchers("/api/v1/auth/sms/**").permitAll() .requestMatchers("/api/v1/docs/**").permitAll() .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() .anyRequest().authenticated() ) .formLogin(AbstractHttpConfigurer::disable) .sessionManagement((sessionManagement) -> sessionManagement .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .exceptionHandling((exception) -> exception .authenticationEntryPoint(customAuthenticationEntryPoint) ) .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(customAuthenticationFilter(), JWTAuthenticationFilter.class); return httpSecurity.build(); } ...
Java
복사

결과 확인

로그인에 실패한 경우를 살펴보겠습니다. 로그인의 경우 회원가입을 완료한 유저가 정상적이지 않은 이메일을 입력하거나, 비밀번호가 틀릴 경우 인증 실패로 AuthenticationException 이 발생하게 됩니다.
if (authException instanceof UsernameNotFoundException) { response.setStatus(HttpStatus.NOT_FOUND.value()); setResponse(response, "AUTH_01"); } else if (authException instanceof BadCredentialsException) { response.setStatus(HttpStatus.UNAUTHORIZED.value()); setResponse(response, "AUTH_02"); }
Java
복사
위와 같이 작성해주었으므로, 특정 유저의 메일이 존재하지 않는 경우 AUTH_01 을 반환할 것입니다. 비밀번호가 틀린 경우 에는AUTH_02 를 반환할 것입니다.

존재하지 않는 이메일인 경우

비밀번호가 틀린 경우