개요AuthenticationEntryPoint처리흐름AccessDeniedHandler에 대한 커스텀 설정AuthenticationEntryPoint 커스텀 설정HttpServletResponse로 바로 내려주기
개요
- FilterSecurityInterceptor 바로 위에 위치하며, FilterSecurityInterceptor 실행 중 발생할 수 있는 예외를 잡고 처리함
- FilterSecurityInterceptor 에서 발생하는
AccessDeniedException
과AuthenticationException
예외를 Http 응답으로 변환해주는 역할을 하는 필터임 AuthenticationException
예외는 인증 관련 예외이며, 사용자를 로그인 페이지로 보냄AccessDeniedException
예외는 AccessDecisionManager에 의해 접근 거부가 발생했을 때 접근 거부 페이지를 보여주거나 사용자를 로그인 페이지로 보냄
필터 체인 상에서 ExceptionTranslationFilter의 위치를 주의해서 볼 필요가 있다. ExceptionTranslationFilter는 필터 체인 실행 스택에서 자기 아래에 오는 필터들에서 발생하는 예외들에 대해서만 처리할 수 있다. 커스텀 필터를 추가해야 하는 경우 이 내용을 잘 기억하고, 커스텀 필터를 적당한 위치에 두어야 한다.
AuthenticationEntryPoint
- 인증되지 않은 사용자 요청을 처리할때 핵심 적인 역할을 수행함 — 보통 사용자를 로그인 요청 페이지로 리다이렉트하는 역할을 함
- 폼 기반 로그인 인증 외의 다른 인증 매커니즘을 처리해야 할때도 AuthenticationEntryPoint를 이용할 수 있음
- 예를 들어 CAS 인증 처리가 필요하다면 CAS 포탈로 사용자를 이동시킴
- 서드 파티 시스템과 연동이 필요한 경우 AuthenticationEntryPoint를 직접 구현할 수도 있음
처리흐름

- ExceptionTranslationFilter doFilter(request, response) 호출
- FilterSecurityInterceptor 에서 Voter들이 투표 진행 (통과시켜도 되는지) FilterSecurityInterceptor
3-1. 예외(AccessDeniedException이나 AuthenticationException) 발생 시, ExceptionTranslationFilter의
sendStartAuthentication()
호출handleSpringSecurityException()
→ handleAuthenticationException()
or handleAccessDeniedException()
→ sendStartAuthentication()
private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, RuntimeException exception) throws IOException, ServletException { if (exception instanceof AuthenticationException) { handleAuthenticationException(request, response, chain, (AuthenticationException) exception); } else if (exception instanceof AccessDeniedException) { handleAccessDeniedException(request, response, chain, (AccessDeniedException) exception); } } private void handleAuthenticationException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AuthenticationException exception) throws ServletException, IOException { this.logger.trace("Sending to authentication entry point since authentication failed", exception); sendStartAuthentication(request, response, chain, exception); } protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AuthenticationException reason) throws ServletException, IOException { // SEC-112: Clear the SecurityContextHolder's Authentication, as the // existing Authentication is no longer considered valid SecurityContext context = SecurityContextHolder.createEmptyContext(); SecurityContextHolder.setContext(context); this.requestCache.saveRequest(request, response); this.authenticationEntryPoint.commence(request, response, reason); }
- SecurityContextHolder 비워짐
HttpServletRequest
가RequestCache
에 저장. 인증이 성공적으로 끝나고 나면RequestCache
가 원본 요청을 수행하기 위해 사용됨
- 클라이언트로부터 credential 정보를 요청하기 위해
AuthenticationEntryPoint
가 사용됨. 예를들어 page의 log로 리다이렉트 할 수도 있고WWW-Authenticate
헤더를 보낼 수도 있음
3-2. 예외 발생하지 않을 시, Filter 다 거쳐서 통과해서 응답 반환

AccessDeniedHandler에 대한 커스텀 설정
@Override protected void configure(HttpSecurity http) throws Exception { http /** * 예외처리 핸들러 */ .exceptionHandling() .accessDeniedHandler((request, response, e) -> { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Object principal = authentication != null ? authentication.getPrincipal() : null; log.warn("{} is denied", principal, accessDeniedException); response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setContentType("text/plain"); response.getWriter().write("## ACCESS DENIED ##"); response.getWriter().flush(); response.getWriter().close(); }) ; }
AccessDeniedHandler가 사용되는 곳 → ExceptionTranslationFilter handleAccessDeniedException()
private void handleAccessDeniedException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AccessDeniedException exception) throws ServletException, IOException { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); boolean isAnonymous = this.authenticationTrustResolver.isAnonymous(authentication); if (isAnonymous || this.authenticationTrustResolver.isRememberMe(authentication)) { if (logger.isTraceEnabled()) { logger.trace(LogMessage.format("Sending %s to authentication entry point since access is denied", authentication), exception); } sendStartAuthentication(request, response, chain, new InsufficientAuthenticationException( this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication", "Full authentication is required to access this resource"))); } else { if (logger.isTraceEnabled()) { logger.trace( LogMessage.format("Sending %s to access denied handler since access is denied", authentication), exception); } this.accessDeniedHandler.handle(request, response, exception); } }
AuthenticationEntryPoint 커스텀 설정
@Configuration public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.cors() .and() .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER) .and() .authorizeRequests() .antMatchers("/accounts/**", "/admin/login", "/docs").permitAll() .antMatchers("/inspections/?", "/inspections").hasRole("ADMIN") .anyRequest().authenticated(); http.exceptionHandling().authenticationEntryPoint( customAuthenticationEntryPoint() ); return http.build(); } private AuthenticationEntryPoint customAuthenticationEntryPoint() { return (request, response, authException) -> { String responseBody = objectMapper.writeValueAsString( ErrorResponse.error(HttpStatus.UNAUTHORIZED, UNAUTHENTICATED.getMessage(), UNAUTHENTICATED.getCode())); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setCharacterEncoding("UTF-8"); PrintWriter writer = response.getWriter(); writer.write(responseBody); writer.flush(); }; } }
HttpServletResponse로 바로 내려주기
[ docs ] Interface ServletResponse
private void makeErrorResponse(HttpServletResponse response) throws IOException { String responseBody = objectMapper.writeValueAsString( ApiResponse.error(HttpStatus.UNAUTHORIZED, EXPIRED_TOKEN.getMessage())); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setCharacterEncoding("UTF-8"); PrintWriter writer = response.getWriter(); writer.write(responseBody); writer.flush(); }