인증 및 인가처리도 중요하지만, 인증 및 인가정보가 유효하지 않을때 사후 처리를 어떻게 진행해야 하는지도 못지 않게 중요하다.
Spring Security는 이러한 인증 및 인가처리를 하지 못하였을때, Exception 처리, 자세하게는 Exception Handling를 진행하여 Servlet Response를 그에 맞게 가공하여 응답처리를 하는 방향으로 진행한다.
이러한 Exception Handling이라 할 수 있는, Spring Security의 인증 및 인가 예외처리를 어떻게 진행할 수 있을지 그 과정에 대해 분석해보았다.
나아가, 전역적인 Global Exception Handling을 개발자가 이해하고 조절할 수 있는 방안에 대해 파악해보았다.
Spring Security는 인증 및 인가처리를 filter chain을 적용하여 진행하는데, 이를 처리하지 못하였을때(인증 및 인가허가를 확보하지 못하였을때) 인증예외(AuthenticationException) 및 인가예외(AccessDeniedException)를 던진다.
예외처리 또한 필터를 통해 처리하며, 이를 위해 ExceptionTranslationFilter를 동작시킨다(이 동작에 403(접근불허) 및 로그인 재시도 요청 등의 방식이 적용된다).
이것이 기본적인 예외 핸들링이고, 위와 같이 크게 인증예외와 인가예외를 진행한다.
인증예외
기본적으로 인증예외가 발생하였다는 것은 ExceptionTranslatorFilter가 동작하여, 인증실패 상황이 발생하였고 해당 예외처리를 발동하였다는 의미이다.
해당 예외가 발동하게 되면,
SecurityContext의 인증정보를 삭제하여, 기존의 authentication을 삭제한다(*삭제 자체 동작, 이후 익명인증 필터가 동작하여 익명인증 authentication 인증정보로 채워진다).
authenticationEntryPoint 호출하여, 인증실패를 공통적으로 처리하도록 구성 가능, 더불어 해당 재시도 url로 리다이렉트하여 로그인(인증)을 재시도 유도하도록 한다.
RequestCache(HttpSessionRequestCache 구현체)에 이전 인증요청 정보를 저장하여, 인증 성공 시 이전의 요청정보를 추출하여 이전의 요청url로 리다이렉트하는 시작점 역할을 해주는 구현체이다.
RequestCache (interface)
↑
└── HttpSessionRequestCache (implements RequestCache)
세번째 부분이 조금 헷갈릴 수 있는데, 직접적인 관련은 없지만 AuthenticationException이 발생하였을때 로그인 재시도를 유도하기 위해 로그인 시도 화면으로 리다이렉트한다. 이때 RequestCache에 이전 요청 정보를 저장하고, 인증 성공 후 이전의 요청경로로 redirect한다.
동작 관점에서 직접적인 연결고리보다는 해당 동작을 발생시키는 연관성 측면으로 이해하면 좋을 것이다.
인가예외
AccessDeniedException 역시 ExceptionTranslatorFilter로 부터 시작하는 예외처리 동작이다.
이 경우 accessDeniedHandler를 호출하여, 사용자가 익명사용자 여부를 판단하여 익명 사용자일때 인증예외처리(AuthenticationException)를 동작하여 동작처리를 위임하고, 익명 사용자가 아닐때 해당 예외동작(AccessDeniedException)을 발생시키며, accessDeniedHandler에게 위임한다.
먼저 개괄이다.
이러한 동작들은 Spring Security에서 exceptionHandling API를 통해 구성해줄 수 있다.
http.exceptionHandling(exception -> exception
.authenticationEntryPoint((req, res, exception) -> {
//authenticationEntry Point 설정
});
.accessDeniedHandler((req, res, exception) -> {
//accessDeniedHandler 설정
});
각각 인증예외처리, 인가예외처리에 대한 내용을 구성해줄 수 있다.
참고로 authenticationEntryPoint는 인증 프로세스마다 기본적으로 제공되는 구현체가 존재하는데,
사용자가 정의하는 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 클래스를 사용해도 되지만, 위와 같이 사용자 정의 인가예외 동작을 정의해줄 수 있겠다.
인증이 필요한 요청을 익명사용자 및 인증을 진행하지 않은 사용자가 한다면, 인증예외를 발생시킨다.
이때 인증예외를 발생시키며, 이 경우

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

사용자 지정의 login페이지로 리다이렉트하여, 인증시도를 유도한다.
물론 이때 RequestCache에 이전의 요청정보를 저장하므로, 인증 성공 후 해당 요청으로 다시 리다이렉트한다.
[1] 사용자가 요청
[2] SecurityContextHolder 에 Authentication = null (로그인 안됨)
[3] Authorization 단계에서 보호된 URL을 확인
[4] 보호된 자원 접근인데 인증 객체가 없음 → AuthenticationException 발생
[5] ExceptionTranslationFilter가 이를 처리
[6] RequestCache.saveRequest() (원래 요청 저장)
[7] AuthenticationEntryPoint.commence() 호출
[8] /login 으로 redirect
인증된 사용자가 권한이 부족한 페이지에 접근하려고 할때

이러한 메시지와 함께

로, 위에서 지정한 페이지로 리다이렉트한다.
authentication = null || authentication = 익명인증사용자, 이 경우일때 authenticationFilter가 authenticationException을 발생시키도록 동작한다.
인증된 사용자이지만 권한 부족, 즉 권한이 충분하지 않을때 AccessDeniedException이 발생한다.
인증예외(AuthenticationException) 및 인가예외(AccessDeniedException)를 감지하여 이를 처리하는 실질적인 동작과 관련한 필터이다.
유의해야 할 점은, 이러한 인증예외 및 인가예외를 발생시키는 시작점은 AuthorizationFilter이며,
의 동작을 함으로써, 해당 예외 처리를 전담하는 실질적인 주체가 바로 ExceptionTranslationFilter이다.
그 후에, http status 상태에 따라 아래와 같은 예외값을 던지기도 한다.
이에 대한 과정을 아래와 같이 간단하게 표현해볼 수 있겠다.
[클라이언트 요청]
|
v
[Filter Chain 시작]
|
v
[AuthorizationFilter]
|
|-- 권한 체크 OK --> 다음 필터 진행
|
|-- 인증/권한 실패 -->
|
v
[ExceptionTranslationFilter]
|
+---------------------------+
| |
v v
[AuthenticationException] [AccessDeniedException]
| |
v v
[AuthenticationEntryPoint] [AccessDeniedHandler]
| |
v v
로그인 페이지/401 403 Forbidden
Authroization은 인증/인가 관련 예외를 던지는 시작점이라 간주해도 무방하며, 이에 따라 예외처리를 실질적으로 진행하는 주체는 ExceptionTranslationFilter라 할 수 있다.
| 상황 | 예외 발생 위치 | 처리 위치 |
|---|---|---|
| 로그인하지 않은 사용자가 보호된 URL 접근 | AuthorizationFilter / FilterSecurityInterceptor | ExceptionTranslationFilter → AuthenticationEntryPoint |
| 로그인한 사용자가 권한 없는 URL 접근 | AuthorizationFilter / FilterSecurityInterceptor | ExceptionTranslationFilter → AccessDeniedHandler |
요청이 발생하였을때 AuthorizationFilter와 ExceptionTranslatorFilter의 상관관계를 중심으로 간략화한 도식을 아래와 같이 정리할 수 있다.
[클라이언트 요청]
|
v
[SecurityContextPersistenceFilter]
| // 세션에서 인증 정보 로드
v
[FilterSecurityInterceptor (AuthorizationFilter 역할)]
|
|-- 인증 없음 -> AuthenticationException 발생
|
v
[ExceptionTranslationFilter]
|
v
[AuthenticationEntryPoint] 호출 → 로그인 페이지로 리다이렉트
결국 ExceptionTranslator라는 실질적인 예외처리 주체를 기반으로 AuthorizationFilter에서 발생시킨 AuthenticationException 및 AccessDeniedException을 처리하는 과정이 인증/인가예외의 큰 흐름이라 볼 수 있겠다.
그 후의 동작은 authenticationEntryPoint에서 설정한 리다이렉트 혹은 accessDeniedHandler에서 설정한 예외 핸들링을 통해 이루어진다.
이 원리를 기반으로, 직접 각각의 entryPoint 및 handler 객체를 만들어 사용하거나 Spring Security에서 제공하는 기본 구현체를 사용하여 예외처리방안을 강구할 수 있겠다.
실무 및 요구사항에 맞게 어떠한 최적의 방안을 적용할 수 있을지 고려하면서 해당 예외처리를 구현하면 될 것이다.