본문 바로가기
웹개발/스프링부트

스프링부트 서블릿 필터 Exception 핸들링

by 어컴띵 2021. 3. 15.

스프링부트에서 exception을 처리할때는 컨트롤러에서 발생한 exception만 핸들링할수 있다. jwt filter를 구현하던중 servlet filter에서 exception이 발생했을때 스프링부트 exception핸들링 처럼 처리하는 방법을 한번 알아본다

 

1. 에러를 핸들링할 filter를 작성한다.

2. jwt filter에서 오류발생시 exception을 던진다.

3. 사용자 exception을 작성한다.

4. ExceptionHandlerFilter를 JwtRequestFilter전에 호출하도록 설정에 등록한다.

5. 오류를 발생 시키고  오류 json을 확인한다.

 

1. 에러를 핸들링할 filter를 작성한다.

서블릿 필터에서 오류 발생시 처리할 excpetion handler를 작성한다.

exception을 캐치해서 ErrorResponse를 셋팅후에 json으로 write를 한다.

@Slf4j
@Component
public class ExceptionHandlerFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
      try{
          filterChain.doFilter(request,response);
      } catch (UsernameFromTokenException ex){
          log.error("exception exception handler filter");
          setErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR,response,ex);
      }catch (RuntimeException ex){
          log.error("runtime exception exception handler filter");
          setErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR,response,ex);
      }
    }

    public void setErrorResponse(HttpStatus status, HttpServletResponse response,Throwable ex){
        response.setStatus(status.value());
        response.setContentType("application/json");
        ErrorResponse errorResponse = new ErrorResponse(ErrorCode.INTER_SERVER_ERROR);
        errorResponse.setMessage(ex.getMessage());
        try{
            String json = errorResponse.convertToJson();
            System.out.println(json);
            response.getWriter().write(json);
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

2. jwt 필더에서 처리할 exception을 던진다.

jwtTokenUtil에서 getUsernameFromToken 메소드 호출시 오류가 발생하면 exception을 던진다.

public String getUsernameFromToken(String token){
        try{
            return getClaimFromToken(token, Claims::getSubject);
        }catch(Exception ex){
            throw new UsernameFromTokenException("username from token exception");
        }
    }
@Slf4j
@Component
public class JwtRequestFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUserDetailsService jwtUserDetailsService;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        final String requestTokenHeader = request.getHeader("Authorization");

        String username = null;
        String jwtToken =null;

        // JWT 토큰은 "Beare token"에 있다. Bearer단어를 제거하고 토큰만 받는다.
        if(requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")){
            jwtToken = requestTokenHeader.substring(7);
            try{
                username = jwtTokenUtil.getUsernameFromToken(jwtToken);
            } catch (IllegalArgumentException ex){
                log.error("Unable to get JWT token", ex);
            } catch (ExpiredJwtException ex){
                log.error("JWT Token has expired", ex);
                throw new ExpiredJwtException("JWT Token has expired");
            } catch (UsernameFromTokenException ex) {
                log.error("token valid error:" + ex.getMessage() ,ex);
                throw new UsernameFromTokenException("Username from token error");
            }
        }else{
            log.warn("JWT token does not begin with Bearer String");
        }

        // 토큰을 가져오면 검증을 한다.
        if(username != null && SecurityContextHolder.getContext().getAuthentication() == null){
            UserDetails userDetails = this.jwtUserDetailsService.loadUserByUsername(username);

            // 토큰이 유효한 경우 수동으로 인증을 설정하도록 스프링 시큐리티를 구성한다.
            if(jwtTokenUtil.validateToken(jwtToken,userDetails)){
                UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
                        new UsernamePasswordAuthenticationToken(
                                userDetails,
                                null,
                                userDetails.getAuthorities()
                        );
                usernamePasswordAuthenticationToken.setDetails(
                        new WebAuthenticationDetailsSource().buildDetails(request)
                );

                // 컨텍스트에 인증을 설정 한 후 현재 사용자가 인증되도록 지정한다.
                // 그래서 Spring Security 설정이 성공적으로 넘어간다.
                SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
            }
        }
        filterChain.doFilter(request,response);
    }
}

3. exception을 작성한다.

사용자가 정의한 exception을 작성한다.

public class UsernameFromTokenException extends RuntimeException{
    public UsernameFromTokenException(String message){
        super(message);
    }
}

4. 필터를 등록한다.

JwtRequestFilter가 호출되기 전에 ExceptionHandlerFilter이 호출되도록 필터를 등록한다.

 

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception{
        return super.authenticationManagerBean();
    }

    @Autowired
    private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

    @Autowired
    private UserDetailsService jwtUserDetailsService;

    @Autowired
    private ExceptionHandlerFilter exceptionHandlerFilter;

    @Autowired
    private JwtRequestFilter jwtRequestFilter;

    @Autowired
    public void configGlobal(AuthenticationManagerBuilder auth) throws Exception{
        // 일치하는 자격증명을 위해 사용자를 로드할 위치를 알수 있도록
        // AuthenticationManager를 구성한다.
        // BCryptPasswordEncoder를 이용
        auth.userDetailsService(jwtUserDetailsService)
                .passwordEncoder(passwordEncoder());
    }

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


    @Override
    protected void configure(HttpSecurity http) throws Exception{
        http
                .cors().disable()
                .csrf().disable()
                // 이 요청은 인증을 하지 않는다.
                .authorizeRequests().antMatchers(
                        "/authenticate",
                "/api/v1/user",
                "/api/v1/authenticate")
                .permitAll()
                // 다른 모든 요청은 인증을 한다.
                .anyRequest().authenticated().and()
                // 상태없는 세션을 이용하며, 세션에 사용자의 상태를 저장하지 않는다.
                .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .formLogin().disable()
                .headers().frameOptions().disable();
        // 모든 요청에 토큰을 검증하는 필터를 추가한다.
        http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
        http.addFilterBefore(exceptionHandlerFilter, JwtRequestFilter.class);
    }
}

5. 오류를 발생 시키고 바뀐 오류내용으로 나오는지 확인한다.

 

받은 인증토큰에서 문자하나를 지우고 호출한다.

스프링부트에서 기본적으로 보여주는 error json

exception handling 추가후 변경된 메세지

 

참조: stackoverflow.com/questions/34595605/how-to-manage-exceptions-thrown-in-filters-in-spring

 

How to manage exceptions thrown in filters in Spring?

I want to use generic way to manage 5xx error codes, let's say specifically the case when the db is down across my whole spring application. I want a pretty error json instead of a stack trace. F...

stackoverflow.com

참조: stackoverflow.com/questions/17715921/exception-handling-for-filter-in-spring

 

exception handling for filter in spring

I am handling exceptions in spring using @ExceptionHandler. Any exception thrown by controller is caught using method annotated with @ExceptionHandler and action is taken accordingly. To avoid writ...

stackoverflow.com