- Docker
- spring boot
- springboot
- 프로그래머스
- SSL
- nginx
- AWS
- PYTHON
- 자바
- join
- sql
- @transactional
- ORM
- spring security 6
- 문자열
- 1차원 배열
- spring
- jpa
- Django
- java
- 데이터베이스
- Git
- spring mvc
- 스프링
- select
- 스프링부트
- session
- string
- DI
- mysql
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
개발하는 자몽
[Spring Security] 예외 처리하기 본문
Notice Spring Boot 3.2.5, Spring Security 6.2.5 기반으로 작성한 글입니다.
아래 포스팅들을 먼저 보면 좋습니다.
- Spring Security 6 - Architecture → Security Exception 처리하기
- [Spring Security] 토큰 기반 로그인/로그아웃 구현하기
이 글의 목표는 Spring Security에서 토큰 기반 인증을 진행할 때 발생한 예외를 `@RestControllerAdvice`로 처리하는 것입니다.
시큐리티 예외 처리 구현
Spring Security에서 예외 처리는 다음과 같은 아키텍처로 이루어진다.
- FilterChain
Spring Security는 `FilterChain`으로 구성되어 있으며, 각 필터는 요청을 처리하거나 다음 필터로 전달한다. 예외가 발생하면 예외 처리 필터가 작동한다. - ExceptionTranslationFilter
해당 필터는 예외를 Spring Security 예외로 변환하고, 적절한 `AuthenticationEntryPoint` 또는 `AccessDeniedHandler`를 호출한다. 이 필터는 이 둘을 주입받아 사용한다. - AuthenticationEntryPoint
`AuthenticationEntryPoint`는 인증되지 않은 요청에 대한 처리를 담당한다. 일반적으로 로그인 페이지로 리디렉션 하거나 401 Unauthorized 응답을 반환한다. - AccessDeniedHandler
`AccessDeniedHandler`는 인가되지 않은 요청(권한 부족)에 대한 처리를 담당한다. 일반적으로 403 Forbidden 응답을 반환한다.
따라서 Spring Security 예외 처리 흐름은 다음과 같다.
- 요청이 들어온다.
- FilterChain에서 예외가 발생한다.
- ExceptionTranslationFilter가 예외를 캐치하고, Spring Security 예외로 변환한다.
- AuthenticationEntryPoint 또는 AccessDeniedHandler가 호출되어 예외를 처리한다.
위 과정처럼 Spring Security는 요청이 컨트롤러에 도달하기 전에 `FilterChain`에서 예외를 발생시킨다.
`@RestControllerAdvice`는 컨트롤러 계층에서 발생하는 예외를 처리하는데, 시큐리티 예외는 컨트롤러에 도달하기 전에 발생하므로 해당 애노테이션으로 처리할 수 없다.
이를 원하는 대로 처리하려면 예외를 인터셉터하는 `AuthenticationEntryPoint`와 `AccessDeniedHandler`를 사용자 정의하여 구현하면 된다. 본격적으로 구현하기 전에 Security Configuration을 설정하자.
SecurityConfig
...
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {
...
private final CustomAuthenticationEntryPoint authenticationEntryPoint;
private final CustomAccessDeniedHandler accessDeniedHandler;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
...
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(e -> e
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler)
)
...
;
return http.build();
}
}
Custom AuthenticationEntryPoint
`AuthenticationEntryPoint` 인터페이스는 인증되지 않은 사용자가 인증이 필요한 요청 엔드포인트로 접근하려 할 때 발생하는 401 Unauthorized 예외를 핸들링 할 수 있도록 도와준다.
...
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final HandlerExceptionResolver resolver;
public CustomAuthenticationEntryPoint(@Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver) {
this.resolver = resolver;
}
@Override
public void commence(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull AuthenticationException authException) {
resolver.resolveException(request, response, null, authException);
}
}
`commence` 메서드는 인증되지 않은 요청이 발생했을 때 호출된다. 여기에서 예외를 처리하는 것이 아닌 `HandlerExceptionResolver`로 넘긴다. 관련 내용은 아래에서 자세하게 다룬다.
Custom AccessDeniedHandler
`AccessDeniedHandler` 인터페이스는 권한이 없는 사용자가 권한이 필요한 요청 엔드포인트로 접근하려 할 때 발생하는 403 Forbidden 예외를 핸들링 할 수 있도록 도와준다.
...
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
private final HandlerExceptionResolver resolver;
public CustomAccessDeniedHandler(@Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver) {
this.resolver = resolver;
}
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
resolver.resolveException(request, response, null, accessDeniedException);
}
}
`handle` 메서드는 권한이 없는 요청이 발생했을 때 호출된다. `AccessDeniedHandler`와 마찬가지로 여기에서 예외를 처리하는 것이 아닌 `HandlerExceptionResolver`로 넘긴다.
HandlerExceptionResovler
`HandlerExceptionResolver`는 Spring Security의 영역이 아닌 Spring MVC 영역에 속해있는 컴포넌트이다.
Spring MVC에서 `HandlerExceptionResolver`는 `DispatcherServlet`의 `HandlerExceptionResolver` 체인(예외 처리 체인)에 등록되어 있다. 이 체인은 컨트롤러에서 발생한 예외를 처리하는 역할을 한다.
따라서 각 커스텀 구현한 `AuthenticationEntryPoint`와 `AccessDeniedHandler`에서 `HandlerExceptionResolver`를 호출하여 컨트롤러에서 예외를 처리할 수 있도록 한다.
HandlerExceptionResolver.resolveException(request, response, null, accessDeniedException);
참고로 위 코드처럼 `handler`를 `null`로 반환하면 다음 `ExceptionResolver`를 찾아서 실행한다. 만약 처리할 수 없는 `ExceptionResolver`가 없으면 예외 처리가 안되고, 기존에 발생한 예외를 서블릿 밖으로 던진다.
우리는 `null`을 반환하여 `@RestControllerAdvice`와 `@ExceptionHandler`를 사용하는 클래스에서 예외를 처리하도록 한다.
Custom ExceptionHandler
이곳에서 Spring Security 예외를 처리한다. 보다 깔끔하게 예외를 가공하고 처리하기 위해서 컨트롤러 계층까지 예외를 가져왔다.
`@RestControllerAdvice`를 사용하여 반환하는 값을 Response Body로 설정하고, 이를 클라이언트에게 전달하도록 한다. `@ExceptionHandler`로 각 적절한 예외를 처리할 수 있도록 한다.
...
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler
public ResponseEntity<ExceptionResponseDto> handleAuthenticationException(AuthenticationException e) {
ExceptionResponseDto dto = new ExceptionResponseDto(HttpStatus.UNAUTHORIZED.name(), ExceptionCode.INVALID_TOKEN.getMessage());
HttpStatus httpStatus = HttpStatus.UNAUTHORIZED;
return ResponseEntity.status(httpStatus).body(dto);
}
@ExceptionHandler
public ResponseEntity<ExceptionResponseDto> handleAccessDeniedException(AccessDeniedException e) {
ExceptionResponseDto dto = new ExceptionResponseDto(HttpStatus.FORBIDDEN.name(), ExceptionCode.ACCESS_DENIED.getMessage());
HttpStatus httpStatus = HttpStatus.FORBIDDEN;
return ResponseEntity.status(httpStatus).body(dto);
}
...
}
클라이언트에게 예외를 반환할 때 필요한 정보만 담을 수 있도록 별도로 DTO를 생성했다.
🔖참고
'Java > Spring' 카테고리의 다른 글
[JPA] 임베디드 타입(@Embeddable, @Embedded)에 관하여 (0) | 2024.08.23 |
---|---|
[Spring Boot / Error] Required request body is missing (0) | 2024.07.13 |
[Spring Security] 역할/권한 구현하기 (0) | 2024.06.14 |
[Spring Security] 토큰 기반 로그인/로그아웃 구현하기 (0) | 2024.06.08 |
Spring Security 6 - Architecture (1) | 2024.06.07 |