에러 처리 - Filter에서 발생한 에러 처리하기

2025. 2. 19. 01:21스프링

Filter에서 JWT에 대한 에러를 처리하려는데, @ControllerAdvice에서 에러를 잡아내지 못해서

제가 만든 ErrorResponse의 형태로 에러를 보내는 것에 문제가 생겼다는 것을 알게되었습니다.

 

Filter는 DispatcherServlet 전에 실행이 되므로 @ControllerAdvice가 발생한 예외를 잡아서 처리할 수 없습니다.

 

기존의 JwtFilter

public class JwtFilter extends OncePerRequestFilter {
    private final JwtValidator jwtValidator;

    public JwtFilter(JwtValidator jwtValidator) {
        this.jwtValidator = jwtValidator;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = getJwtFromRequest(request);
        
        if(token == null) throw new CustomException(ErrorCode.TOKEN_NOT_PROVIDED);

        if(!jwtValidator.isExpired(token, true)) {
            String username = jwtValidator.getUsername(token, true);
            String role = jwtValidator.getRole(token, true);
            Role roleEnum = Role.valueOf(role);

            UserEntity userEntity = new UserEntity();
            userEntity.setUsername(username);
            userEntity.setPassword(null);
            userEntity.setRole(roleEnum);

            CustomUserDetails customUserDetails = new CustomUserDetails(userEntity);
            UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities());

            SecurityContextHolder.getContext().setAuthentication(authToken);

            filterChain.doFilter(request, response);
        } else {
            // 토큰이 유효하지 않다면 401 Unauthorized 응답
            throw new CustomException(ErrorCode.EXPIRED_ACCESS_TOKEN);
        }
    }

    private String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if(StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

 

 

 


방법 1. Filter 내부에서 처리

try-catch 문을 이용해서 Filter 내부에서 처리를 하면 아래와 같습니다.

- 변경된 JwtFilter

public class JwtFilter extends OncePerRequestFilter {
    private final JwtValidator jwtValidator;

    private final ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());

    public JwtFilter(JwtValidator jwtValidator) {
        this.jwtValidator = jwtValidator;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            String token = getJwtFromRequest(request);

            if (token == null) throw new CustomException(ErrorCode.TOKEN_NOT_PROVIDED);

            if (!jwtValidator.isExpired(token, true)) {
                String username = jwtValidator.getUsername(token, true);
                String role = jwtValidator.getRole(token, true);
                Role roleEnum = Role.valueOf(role);

                UserEntity userEntity = new UserEntity();
                userEntity.setUsername(username);
                userEntity.setPassword(null);
                userEntity.setRole(roleEnum);

                CustomUserDetails customUserDetails = new CustomUserDetails(userEntity);
                UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities());

                SecurityContextHolder.getContext().setAuthentication(authToken);

                filterChain.doFilter(request, response);
            } else {
                throw new CustomException(ErrorCode.EXPIRED_ACCESS_TOKEN);
            }
        } catch (CustomException e) {
            setErrorResponse(response, e.getErrorCode(), request.getRequestURI());
        }
    }

    private void setErrorResponse(HttpServletResponse response, ErrorCode errorCode, String path) throws IOException {
        response.setStatus(errorCode.getHttpStatus().value());
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");

        ErrorResponse errorResponse = new ErrorResponse(
                errorCode.getHttpStatus(),
                errorCode.getCode(),
                errorCode.getMessage(),
                path
        );

        response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
    }

    private String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if(StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

 

이렇게 catch문에서 원하는 형태로 만들어서 클라이언트에게 응답해주면 됩니다.

 

 

 

 


방법 2. 전역 예외 처리를 하는 Filter를 추가하기

- GlobalExceptionFilter

@Component
public class GlobalExceptionFilter extends OncePerRequestFilter {
    private final ObjectMapper objectMapper;

    public GlobalExceptionFilter(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            filterChain.doFilter(request, response);
        } catch (CustomException e) {
            setErrorResponse(response, e.getErrorCode(), request.getRequestURI());
        } catch (Exception e) {
            setErrorResponse(response, ErrorCode.INTERNAL_SERVER_ERROR, request.getRequestURI());
        }
    }

    private void setErrorResponse(HttpServletResponse response, ErrorCode errorCode, String path) throws IOException {
        response.setStatus(errorCode.getHttpStatus().value());
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");

        ErrorResponse errorResponse = new ErrorResponse(
                errorCode.getHttpStatus(),
                errorCode.getCode(),
                errorCode.getMessage(),
                path
        );

        response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
    }
}

 

- SecurityConfig

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    private final AuthenticationConfiguration authenticationConfiguration;
    private final JwtProvider jwtProvider;
    private final JwtValidator jwtValidator;
    private final GlobalExceptionFilter globalExceptionFilter;


    public SecurityConfig(AuthenticationConfiguration authenticationConfiguration,
                          JwtProvider jwtProvider,
                          JwtValidator jwtValidator,
                          GlobalExceptionFilter globalExceptionFilter) {
        this.authenticationConfiguration = authenticationConfiguration;
        this.jwtProvider = jwtProvider;
        this.jwtValidator = jwtValidator;
        this.globalExceptionFilter = globalExceptionFilter;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .formLogin(form -> form.disable())
            .authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
            .addFilterAt(new LoginFilter(authenticationManager(this.authenticationConfiguration), this.jwtProvider), UsernamePasswordAuthenticationFilter.class)
            .addFilterBefore(new JwtFilter(jwtValidator), LoginFilter.class)
            .addFilterBefore(globalExceptionFilter, JwtFilter.class);

        return http.build();
    }
}

 

이렇게 FilterChain의 제일 첫번 째 필터로 등록해 요청을 감싸서 예외를 잡도록 설정합니다.

이렇게 한다면 이 필터이후에 등록된 모든 필터에서 발생한 예외가 이 필터에서 처리되므로 일관된 에러 응답 유지가 가능합니다.