일반적으로 예외는 클라이언트 측에서 서버로 보낸 요청을 애플리케이션의 컨트롤러가 받은 후 요청에 대한 비즈니스 로직을 처리하는 과정에서 발생한다. 즉 요청이 컨트롤러에 도달한 다음 예외가 발생하는 것이다.
하지만 스프링 시큐리티는 요청이 컨트롤러에 도달하기 전에 필터 체인에서 예외를 발생시킨다. 따라서 @ControllerAdvice는 컨트롤러 계층에서 발생하는 예외를 처리하기 때문에 시큐리티의 예외를 처리할 수 없다.
스프링 시큐리티는 사용자가 인증되지 않거나 AuthenticationException이 발생했을 때 AuthenticationEntryPoint에서 예외 처리를 시도한다. 따라서 AuthenticationEntryPoint의 구현체를 이용하여 스프링 시큐리티 예외를 처리할 수 있다.

SecurityConfig에 ExceptionHandling 적용
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity // Spring Security 지원을 가능하게 함
public class SecurityConfig {
private final JwtUtil jwtUtil;
private final UserDetailsServiceImpl userDetailsService;
private final UserRepository userRepository;
private final AuthenticationConfiguration authenticationConfiguration;
private final CustomAccessDeniedHandler customAccessDeniedHandler;
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
JwtAuthenticationFilter filter = new JwtAuthenticationFilter(jwtUtil, userRepository);
filter.setAuthenticationManager(authenticationManager(authenticationConfiguration));
return filter;
}
@Bean
public JwtAuthorizationFilter jwtAuthorizationFilter() {
return new JwtAuthorizationFilter(jwtUtil, userDetailsService);
}
@Bean
public JwtExceptionFilter jwtExceptionFilter() {
return new JwtExceptionFilter();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// CSRF 설정
http.csrf((csrf) -> csrf.disable());
// 기본 설정인 Session 방식은 사용하지 않고 JWT 방식을 사용하기 위한 설정
http.sessionManagement((sessionManagement) ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
http.authorizeHttpRequests((authorizeHttpRequests) ->
authorizeHttpRequests
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
.requestMatchers("/").permitAll() // 요청 허가
.requestMatchers(HttpMethod.POST, "/users").permitAll()
.requestMatchers("/users/login").permitAll()
.requestMatchers(HttpMethod.GET, "/posts").permitAll()
.anyRequest().authenticated() // 그 외 모든 요청 인증처리
);
// security 예외처리
http.exceptionHandling((exception) -> exception
.accessDeniedHandler(customAccessDeniedHandler)
.authenticationEntryPoint(customAuthenticationEntryPoint));
http.addFilterBefore(jwtAuthorizationFilter(), JwtAuthenticationFilter.class);
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(jwtExceptionFilter(), JwtAuthorizationFilter.class);
return http.build();
}
}
AuthenticationEntryPoint 구현체
/**
* 인증 되지않은 유저 요청 시 동작
*/
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
ExceptionDto exceptionDto = new ExceptionDto(ErrorType.REQUIRES_LOGIN);
response.setStatus(exceptionDto.getErrorType().getHttpStatus().value());
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(new ObjectMapper().writeValueAsString(exceptionDto));
}
}