[개발지식] Web Application의 GateKeeper #10 - 인증 및 인가 예외처리 과정 분석(*ExceptionTranslatorFilter를 중심으로)

이효균·2025년 12월 1일

개발지식

목록 보기
111/131

1. 개요

인증 및 인가처리도 중요하지만, 인증 및 인가정보가 유효하지 않을때 사후 처리를 어떻게 진행해야 하는지도 못지 않게 중요하다.

Spring Security는 이러한 인증 및 인가처리를 하지 못하였을때, Exception 처리, 자세하게는 Exception Handling를 진행하여 Servlet Response를 그에 맞게 가공하여 응답처리를 하는 방향으로 진행한다.

이러한 Exception Handling이라 할 수 있는, Spring Security의 인증 및 인가 예외처리를 어떻게 진행할 수 있을지 그 과정에 대해 분석해보았다.

나아가, 전역적인 Global Exception Handling을 개발자가 이해하고 조절할 수 있는 방안에 대해 파악해보았다.

2. Exception Handling

Spring Security는 인증 및 인가처리를 filter chain을 적용하여 진행하는데, 이를 처리하지 못하였을때(인증 및 인가허가를 확보하지 못하였을때) 인증예외(AuthenticationException) 및 인가예외(AccessDeniedException)를 던진다.

예외처리 또한 필터를 통해 처리하며, 이를 위해 ExceptionTranslationFilter를 동작시킨다(이 동작에 403(접근불허) 및 로그인 재시도 요청 등의 방식이 적용된다).

이것이 기본적인 예외 핸들링이고, 위와 같이 크게 인증예외와 인가예외를 진행한다.

2-1. AuthenticationException

인증예외

기본적으로 인증예외가 발생하였다는 것은 ExceptionTranslatorFilter가 동작하여, 인증실패 상황이 발생하였고 해당 예외처리를 발동하였다는 의미이다.

해당 예외가 발동하게 되면,

  • SecurityContext의 인증정보를 삭제하여, 기존의 authentication을 삭제한다(*삭제 자체 동작, 이후 익명인증 필터가 동작하여 익명인증 authentication 인증정보로 채워진다).

  • authenticationEntryPoint 호출하여, 인증실패를 공통적으로 처리하도록 구성 가능, 더불어 해당 재시도 url로 리다이렉트하여 로그인(인증)을 재시도 유도하도록 한다.

  • RequestCache(HttpSessionRequestCache 구현체)에 이전 인증요청 정보를 저장하여, 인증 성공 시 이전의 요청정보를 추출하여 이전의 요청url로 리다이렉트하는 시작점 역할을 해주는 구현체이다.

RequestCache (interface)
        ↑
        └── HttpSessionRequestCache (implements RequestCache)

세번째 부분이 조금 헷갈릴 수 있는데, 직접적인 관련은 없지만 AuthenticationException이 발생하였을때 로그인 재시도를 유도하기 위해 로그인 시도 화면으로 리다이렉트한다. 이때 RequestCache에 이전 요청 정보를 저장하고, 인증 성공 후 이전의 요청경로로 redirect한다.

동작 관점에서 직접적인 연결고리보다는 해당 동작을 발생시키는 연관성 측면으로 이해하면 좋을 것이다.

2-2. AccessDeniedException

인가예외

AccessDeniedException 역시 ExceptionTranslatorFilter로 부터 시작하는 예외처리 동작이다.

이 경우 accessDeniedHandler를 호출하여, 사용자가 익명사용자 여부를 판단하여 익명 사용자일때 인증예외처리(AuthenticationException)를 동작하여 동작처리를 위임하고, 익명 사용자가 아닐때 해당 예외동작(AccessDeniedException)을 발생시키며, accessDeniedHandler에게 위임한다.

2-3. exceptionHandling API

먼저 개괄이다.

이러한 동작들은 Spring Security에서 exceptionHandling API를 통해 구성해줄 수 있다.

http.exceptionHandling(exception -> exception
	.authenticationEntryPoint((req, res, exception) -> {
    	//authenticationEntry Point 설정
    });
	.accessDeniedHandler((req, res, exception) -> {
    	//accessDeniedHandler 설정
    });

각각 인증예외처리, 인가예외처리에 대한 내용을 구성해줄 수 있다.

참고로 authenticationEntryPoint는 인증 프로세스마다 기본적으로 제공되는 구현체가 존재하는데,

  • UsernamePasswordAuthenticationFilter는 LoginUrlAuthenticationEntryPoint를 제공하여 로그인페이지로의 리다이렉트 시도
  • BasicAuthenticationFilter는 BasicAuthenticationEntryPoint를 제공하며
  • 이외 기본적으로 제공해주는 entryPoint로 Http403ForbiddenEntryPoint를 제공한다.

사용자가 정의하는 AuthenticationEntryPoint 구현이 가장 우선적으로 수행되며, 이경우 기본적인 entryPoint 설정 정보는 무시된다.

인가예외를 담당하는 accessDeniedHandler의 구현체는 AccessDeniedHandlerImple이다.

본격적으로 해당 api를 다뤄보자.

http
                .authorizeRequests(auth -> auth
                        .requestMatchers("/login", "/invalid").permitAll()
                        .anyRequest().authenticated())
                .formLogin(Customizer.withDefaults())
                .exceptionHandling(exception
                        -> exception.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint)

의 로직에서

.exceptionHandling(exception
                        -> exception.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint)

exceptionHandling을 통해 기본적으로 제공해주는 entryPoint 구현체를 설정해줄 수 있다.

http
                .authorizeRequests(auth -> auth
                        .requestMatchers("/login", "/invalid").permitAll()
                        .anyRequest().authenticated())
                .formLogin(Customizer.withDefaults())
                .exceptionHandling(exception
                        -> exception.authenticationEntryPoint(new AuthenticationEntryPoint() {
                            @Override
                            public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
                                System.out.println("exception : " + authException.getMessage());
                                response.sendRedirect("/login");
                            }
                }))

더불어, new authenticationEntryPoint 구현체를 지정해주어 사용자지정 entry point를 구성해줄 수도 있다(위의 경우 /login 페이지로 리다이렉트).

추가적으로 exception api에 대해 accessDeniedHandler까지 구성하여, 인가예외처리 동작을 지정해줄 수 있다.

.accessDeniedHandler(new AccessDeniedHandler() {
                            @Override
                            public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
                                System.out.println("accessDenied : " + accessDeniedException.getMessage());
                                response.sendRedirect("/denied");
                            }
                        })

물론 기본 구현체인 AccessDeniedHandlerImple 클래스를 사용해도 되지만, 위와 같이 사용자 정의 인가예외 동작을 정의해줄 수 있겠다.

2-4. handling 동작

  1. 인증예외

인증이 필요한 요청을 익명사용자 및 인증을 진행하지 않은 사용자가 한다면, 인증예외를 발생시킨다.

이때 인증예외를 발생시키며, 이 경우

위와 같이, 말 그대로 인증이 필요하다는 message와 함께

사용자 지정의 login페이지로 리다이렉트하여, 인증시도를 유도한다.

물론 이때 RequestCache에 이전의 요청정보를 저장하므로, 인증 성공 후 해당 요청으로 다시 리다이렉트한다.

[1] 사용자가 요청
[2] SecurityContextHolder 에 Authentication = null (로그인 안됨)
[3] Authorization 단계에서 보호된 URL을 확인
[4] 보호된 자원 접근인데 인증 객체가 없음 → AuthenticationException 발생
[5] ExceptionTranslationFilter가 이를 처리
[6] RequestCache.saveRequest() (원래 요청 저장)
[7] AuthenticationEntryPoint.commence() 호출
[8] /login 으로 redirect
  1. 인가예외

인증된 사용자가 권한이 부족한 페이지에 접근하려고 할때

이러한 메시지와 함께

로, 위에서 지정한 페이지로 리다이렉트한다.

authentication = null || authentication = 익명인증사용자, 이 경우일때 authenticationFilter가 authenticationException을 발생시키도록 동작한다.

인증된 사용자이지만 권한 부족, 즉 권한이 충분하지 않을때 AccessDeniedException이 발생한다.

3. ExceptionTranslationFilter

인증예외(AuthenticationException) 및 인가예외(AccessDeniedException)를 감지하여 이를 처리하는 실질적인 동작과 관련한 필터이다.

유의해야 할 점은, 이러한 인증예외 및 인가예외를 발생시키는 시작점은 AuthorizationFilter이며,

  • 인증이 필요하지만 인증되지 않은 경우 → AuthenticationException 발생
  • 인증은 되었지만 권한 부족 → AccessDeniedException 발생

의 동작을 함으로써, 해당 예외 처리를 전담하는 실질적인 주체가 바로 ExceptionTranslationFilter이다.

그 후에, http status 상태에 따라 아래와 같은 예외값을 던지기도 한다.

  • 인증 실패 → 로그인 페이지로 리다이렉트 또는 401 응답한다(AuthenticationException).
  • 접근 거부 → 403 응답한다(AccessDeniedException).

이에 대한 과정을 아래와 같이 간단하게 표현해볼 수 있겠다.

[클라이언트 요청]
        |
        v
[Filter Chain 시작]
        |
        v
[AuthorizationFilter]
        |
        |-- 권한 체크 OK --> 다음 필터 진행
        |
        |-- 인증/권한 실패 -->
                  |
                  v
        [ExceptionTranslationFilter]
                  |
      +---------------------------+
      |                           |
      v                           v
[AuthenticationException]   [AccessDeniedException]
      |                           |
      v                           v
[AuthenticationEntryPoint]  [AccessDeniedHandler]
      |                           |
      v                           v
   로그인 페이지/401          403 Forbidden
   

3-1. 로그인하지 않은 사용자가 특정 페이지로 요청을 할 때의 예외도 최초 시작점은 AuthorizationFilter이다.

Authroization은 인증/인가 관련 예외를 던지는 시작점이라 간주해도 무방하며, 이에 따라 예외처리를 실질적으로 진행하는 주체는 ExceptionTranslationFilter라 할 수 있다.

상황예외 발생 위치처리 위치
로그인하지 않은 사용자가 보호된 URL 접근AuthorizationFilter / FilterSecurityInterceptorExceptionTranslationFilter → AuthenticationEntryPoint
로그인한 사용자가 권한 없는 URL 접근AuthorizationFilter / FilterSecurityInterceptorExceptionTranslationFilter → AccessDeniedHandler

요청이 발생하였을때 AuthorizationFilter와 ExceptionTranslatorFilter의 상관관계를 중심으로 간략화한 도식을 아래와 같이 정리할 수 있다.

[클라이언트 요청]
        |
        v
[SecurityContextPersistenceFilter]
        |   // 세션에서 인증 정보 로드
        v
[FilterSecurityInterceptor (AuthorizationFilter 역할)]
        |
        |-- 인증 없음 -> AuthenticationException 발생
                  |
                  v
[ExceptionTranslationFilter]
        |
        v
[AuthenticationEntryPoint] 호출 → 로그인 페이지로 리다이렉트

4. 결론

결국 ExceptionTranslator라는 실질적인 예외처리 주체를 기반으로 AuthorizationFilter에서 발생시킨 AuthenticationException 및 AccessDeniedException을 처리하는 과정이 인증/인가예외의 큰 흐름이라 볼 수 있겠다.

그 후의 동작은 authenticationEntryPoint에서 설정한 리다이렉트 혹은 accessDeniedHandler에서 설정한 예외 핸들링을 통해 이루어진다.

이 원리를 기반으로, 직접 각각의 entryPoint 및 handler 객체를 만들어 사용하거나 Spring Security에서 제공하는 기본 구현체를 사용하여 예외처리방안을 강구할 수 있겠다.

실무 및 요구사항에 맞게 어떠한 최적의 방안을 적용할 수 있을지 고려하면서 해당 예외처리를 구현하면 될 것이다.

profile
저의 고민, 설계 과정을 담고 있습니다.

0개의 댓글