예외 처리
Spring Security의 인증·인가 예외를 감지하고 적절한 응답으로 처리하는 메커니즘
목차
ExceptionTranslationFilter가 중앙에서 포착한다.
예외는 크게 인증 실패(AuthenticationException)와 인가 실패(AccessDeniedException)로 구분되며,
각각 AuthenticationEntryPoint와 AccessDeniedHandler로 위임된다.
요청 정보는 HttpSessionRequestCache에 저장되어 인증 성공 후 원래 URL로 리다이렉트할 수 있다.
1. 예외 유형
AuthenticationException — 인증 실패
인증되지 않은 사용자가 보호된 리소스에 접근하거나, 인증 과정 자체에서 오류가 발생할 때 던져진다.
SecurityContext에서 인증 정보를 초기화하고, 원래 요청을 RequestCache에 저장한 뒤
AuthenticationEntryPoint를 호출하여 로그인 페이지로 리다이렉트하거나 401 응답을 반환한다.
BadCredentialsException— 잘못된 자격증명UsernameNotFoundException— 사용자를 찾을 수 없음InsufficientAuthenticationException— 인증 수준 부족 (Anonymous 상태로 접근)SessionAuthenticationException— 세션 인증 처리 실패
AccessDeniedException — 인가 실패
인증된 사용자가 권한이 부족한 리소스에 접근할 때 던져진다.
ExceptionTranslationFilter는 이 예외를 받으면 먼저 현재 사용자가 익명(Anonymous) 또는 RememberMe 인증인지 확인한다.
익명이나 RememberMe라면 완전한 인증이 아니므로 AuthenticationEntryPoint로 넘겨 재인증을 요청하고,
완전히 인증된 사용자라면 AccessDeniedHandler를 호출하여 403 응답을 반환한다.
AccessDeniedException— 권한 없음 (403 Forbidden)
AuthenticationException은 즉시 EntryPoint로 라우팅된다.
AccessDeniedException은 익명/RememberMe 여부를 먼저 확인하여,
미완전 인증이면 EntryPoint(재인증)로, 완전 인증이면 AccessDeniedHandler(403)로 분기한다.
2. exceptionHandling() API
기본 설정
exceptionHandling()을 통해 AuthenticationEntryPoint와 AccessDeniedHandler를 커스터마이징할 수 있다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login").permitAll()
.anyRequest().authenticated()
)
.exceptionHandling(ex -> ex
// 인증되지 않은 사용자가 접근할 때 호출 (AuthenticationException)
.authenticationEntryPoint((request, response, authException) -> {
response.sendRedirect("/login");
})
// 인증은 됐으나 권한이 없을 때 호출 (AccessDeniedException)
.accessDeniedHandler((request, response, accessDeniedException) -> {
response.sendRedirect("/access-denied");
})
);
return http.build();
}
커스텀 구현체 등록
// AuthenticationEntryPoint 구현
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
// JSON API 서버라면 401 응답
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("{\"error\": \"Unauthorized\"}");
}
}
// AccessDeniedHandler 구현
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write("{\"error\": \"Forbidden\"}");
}
}
3. ExceptionTranslationFilter 흐름
필터 위치와 역할
ExceptionTranslationFilter는 Spring Security 필터 체인의 후반부에 위치하며,
자신보다 뒤에 오는 AuthorizationFilter(인가 필터)에서 발생한 예외를 catch한다.
인증·인가 예외를 적절한 처리기로 라우팅하는 중간 다리 역할을 한다.
동작 흐름
내부 동작 코드 레벨
ExceptionTranslationFilter는 doFilter() 내에서 try-catch 블록으로
이후 필터 체인 실행을 감싸고, 예외 발생 시 handleSpringSecurityException()으로 분기한다.
// ExceptionTranslationFilter 핵심 로직 (요약)
public class ExceptionTranslationFilter extends GenericFilterBean {
private AuthenticationEntryPoint authenticationEntryPoint;
private AccessDeniedHandler accessDeniedHandler;
private RequestCache requestCache = new HttpSessionRequestCache();
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
try {
// 다음 필터(AuthorizationFilter 등) 실행
chain.doFilter(req, res);
} catch (IOException ex) {
throw ex;
} catch (Exception ex) {
// Spring Security 예외 추출
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
RuntimeException securityException =
(AuthenticationException) throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (securityException == null) {
securityException =
(AccessDeniedException) throwableAnalyzer
.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
}
if (securityException != null) {
handleSpringSecurityException(request, response, chain, securityException);
} else {
throw ex;
}
}
}
private void handleSpringSecurityException(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
RuntimeException exception)
throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
// 1. SecurityContext 초기화
SecurityContextHolder.clearContext();
// 2. 원래 요청을 세션에 저장 (로그인 성공 후 리다이렉트용)
requestCache.saveRequest(request, response);
// 3. AuthenticationEntryPoint 호출 (로그인 페이지로)
authenticationEntryPoint.commence(request, response, (AuthenticationException) exception);
} else if (exception instanceof AccessDeniedException) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// Anonymous 또는 RememberMe 사용자라면 → 인증 예외와 동일하게 처리
if (authenticationTrustResolver.isAnonymous(authentication)
|| authenticationTrustResolver.isRememberMe(authentication)) {
requestCache.saveRequest(request, response);
authenticationEntryPoint.commence(request, response,
new InsufficientAuthenticationException("Full authentication is required"));
} else {
// 실제 인증된 사용자 → AccessDeniedHandler 호출 (403)
accessDeniedHandler.handle(request, response, (AccessDeniedException) exception);
}
}
}
}
4. HttpSessionRequestCache와 요청 저장
개념
인증이 필요한 요청이 들어왔을 때, 사용자를 로그인 페이지로 리다이렉트하기 전에
원래 요청 정보(URL, 파라미터, 헤더)를 세션에 저장한다.
로그인 성공 후 SavedRequestAwareAuthenticationSuccessHandler가 이 정보를 읽어
원래 URL로 리다이렉트한다.
DefaultSavedRequest
HttpSessionRequestCache는 요청을 DefaultSavedRequest로 감싸
세션에 SPRING_SECURITY_SAVED_REQUEST 키로 저장한다.
저장된 요청 정보에는 URL, 쿼리 파라미터, 헤더, 쿠키 등이 포함된다.
// RequestCache 커스터마이징
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
// "continue" 파라미터가 없는 요청은 캐시하지 않음
requestCache.setMatchingRequestParameterName("continue");
http
.requestCache(cache -> cache.requestCache(requestCache))
.exceptionHandling(ex -> ex
.authenticationEntryPoint(customEntryPoint)
);
return http.build();
}
// 로그인 성공 후 저장된 요청으로 리다이렉트
@Bean
public AuthenticationSuccessHandler successHandler(RequestCache requestCache) {
SavedRequestAwareAuthenticationSuccessHandler handler =
new SavedRequestAwareAuthenticationSuccessHandler();
handler.setRequestCache(requestCache);
handler.setDefaultTargetUrl("/dashboard");
return handler;
}
흐름 요약
정리
- ExceptionTranslationFilter는 AuthorizationFilter 이후 예외를 캐치하는 중앙 예외 처리기다.
- AuthenticationException → SecurityContext 초기화 → RequestCache 저장 → AuthenticationEntryPoint(로그인 유도)
- AccessDeniedException → 익명 또는 RememberMe면 AuthenticationEntryPoint(재인증 유도), 완전 인증된 사용자면 AccessDeniedHandler(403)
- HttpSessionRequestCache가 원래 요청을 보존하여 로그인 성공 후 원래 URL로 복귀할 수 있게 한다.
- REST API 서버라면 EntryPoint와 Handler 모두 JSON 응답(401/403)을 반환하도록 커스터마이징한다.
'Java & Spring > Spring Security' 카테고리의 다른 글
| 08. 인증 프로세스 전체 흐름 (0) | 2026.08.06 |
|---|---|
| 07. 악용 보호 (0) | 2026.08.06 |
| 05. 세션 관리 (0) | 2026.08.05 |
| 04. 인증 상태 영속성 (0) | 2026.08.04 |
| 03. 인증 아키텍처 (0) | 2026.08.04 |