Web/Spring

[Spring Security] jwt 401 응답 + jwt 예외 핸들러 만들기

수짱수짱 2023. 11. 29. 23:16

일단 security 예외는 사용자가 만든 spring 핸들러에 걸리지 않는다

이유는 security가 spring이 실행되기 전보다 먼저 실행되기 때문이다..

사용자가 만든 핸들러는 spring에 걸려있고 security가 그것보다 더 전에 실행하기 때문에 걸리지 않는다

위 그림처럼 Filter가 Request의 가장 앞단에 위치해있고 HnadlerInterceptor가 controller단에 존재한다

 

이래서 이 게시글을 작성하기 전까진 jwtFilter에서 발생하는 요런 에러들이 GlobalHandler에 잡히지 않았다

(사실 Spring의 GlobalHandler에 잡히는 줄 알았고 코드 문제로 동작이 안 하는 줄 알았다..._)

 

그래서 겸사겸사 위와 같은 문제들까지 다 해결하기위해 해당 게시글을 작성하고자 한다!

 

 

 

Spring Security Exception Handling - Filter 단 예외 처리하기

오늘은 Spring Security 를 적용했지만 JWT 가 만료되거나, 잘못된 토큰일 경우 401 코드 뿐만아니라 에러 메세지까지 핸들링 해줄 수 있도록 설정해 주고자 합니다. 1. Spring Security 와 @ControllerAdvice 먼

thalals.tistory.com

가장 도움이 많이 된 블로그는 요기! 🙇‍♂️🙇‍♂️🙇‍♂️

 

전체적인 로직은 위 블로그에서 알려주신대로 위와 같이 진행하려고 한다

즉, 현재 jwtFilter가 걸려있는 쪽보다 더 앞쪽에 Filter를 걸어서 해당 Filter에선 Exception을 체크하고 핸들링해주는 것이다

여기서 A필터는 원래 있던 JwtFilter가 되는 것이고 B필터는 새롭게 추가해줄 ExceptionHandlerFilter가 되는 것이다.

즉, ExceptionHandlerFilter가 먼저❗exception을 체크하고 response에 담아 클라이언트에게 전달해준다

따라서 Filter간의 순서가 중요한데 이때 SecurityConfig에 해당 필터들의 순서에 유의하여 설정해 주어야한다~! → 3️⃣에서 참고!

1️⃣ JwtFilter 내부 isValidToken 메소드에서 각 jwt 에러에 따라 예외처리 추가

각 jwt 예외 상황마다 new JwtException으로 커스텀한 예외 메시지를 넣어주고 있다.

참고로 TODO에 적힌 포스트맨 로그인 요청시 jwt 유효성 오류는 시큐리티 설정이 잘못된 줄 알았는데 그게 아니라 포스트맨 문제라고 한다! 그래서 임시 조치로 예외처리를 뺐다

(참고: https://tte-yeong.tistory.com/129)

 

 

여기서 중요한 점! ✅

 

JwtFilter의 doFilterInternal 메소드에서 아래와 같이 예외 처리를 한다면??..

기껏 만든 ExceptionHandlerFilter 가 아니라 JwtFilter에서 예외를 핸들러하는 것이 되므로 JwtFilter에서는 try/catch로 예외 처리를 하지 않도록 하자 (경험담…)

 

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

        try {
            if (jwtTokenProvider.isValidToken(token)) {
                Authentication authentication = jwtTokenProvider.getAuthentication(token);
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }catch(JwtException e) {
            // 예외 처리 구문..
        }

        filterChain.doFilter(request, response);
    }

// 이러면 jwtFilter에서 예외처리를 하게되어 원하는 결과를 얻을 수 없다! 주의!!

 

2️⃣ ExceptionHandlerFilter 를 만들자

 

이제 1️⃣에서 발생하는 JwtException을 처리하기 위한 jwt 예외 핸들러를 만들어보자

즉, 앞서 말한 B필터가 되겠다!

@Slf4j
@Component
public class ExceptionHandlerFilter extends OncePerRequestFilter { // 새롭게 생성!

    record ErrorResponse(
        String messsage
    ) {
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
        FilterChain filterChain) throws ServletException, IOException {
        try {
            filterChain.doFilter(request, response);
        } catch (JwtException ex) {
            setErrorResponse(HttpStatus.UNAUTHORIZED, request, response, ex);
        }
    }

    public void setErrorResponse(HttpStatus status, HttpServletRequest request,
        HttpServletResponse response, Throwable ex) throws IOException {
        ObjectMapper objectMapper = new ObjectMapper();

        response.setStatus(status.value());
        response.setContentType("application/json; charset=UTF-8");

        ErrorResponse errorResponse = new ErrorResponse(ex.getMessage());
        log.error(ex.getMessage());

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

따라서 jwt 예외가 발생하면 앞서 정의해둔 예외 응답 프로토콜에 따라 응답 객체를 생성하고 ObjectMapper를 이용하여 클라이언트에게 오류를 Json 형태로 반환하는 로직이다!

 

3️⃣ SecurityConfig 설정

이제 앞에서 만든(2️⃣) jwt 예외 핸들러를 security에 설정해주는 작업을 하자!

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .cors().configurationSource(corsConfigurationSource())
        .and()
        .csrf().disable()
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
        .formLogin().disable()
        .httpBasic().disable()
        .authorizeRequests()
        .antMatchers(permitUrls).permitAll()
        .and()
        .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) 
        .addFilterBefore(exceptionHandlerFilter, JwtFilter.class);  // = exceptionHandlerFilter가 JwtFilter 보다 먼저 실행된다

        return http.build();
}

가장 중요한 것은 addFilterBefore 메소드의 인자 순서이다.

 

.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)

  • UsernamePasswordAuthenticationFilter 보다 jwtFilter가 우선이다

.addFilterBefore(exceptionHandlerFilter, JwtFilter.class)

  • JwtFilter 보다 exceptionHandlerFilter 가 우선이다 ⭐⭐⭐
  • 즉, 위에서 말한 로직대로 B필터가 우선이 되려면 해당 설정이 필수적이다!!

위에 올려다보기 귀찮을까봐..

 

더보기

addFilterBefore에 관해 gpt 참고

 

addFilterBefore 메소드는 Spring Security에서 사용되는 메소드 중 하나로 보안 필터를 특정 필터 이전에 추가하는 데 사용됩니다.

이 메소드는 FilterChainProxy에 의해 호출되며, 보통 Spring Security 설정 클래스에서 사용됩니다.

 

 

1. Filter 등록 순서의 중요성

Spring Security는 여러 보안 필터들이 체인 형태로 구성되어 있습니다.

각 필터는 특정한 작업을 수행하며, 필터 체인은 클라이언트의 요청에 대한 처리를 담당합니다. 

addFilterBefore 메소드를 사용하여 필터를 특정 필터 이전에 추가함으로써 필터의 실행 순서를 제어할 수 있습니다.

 

2. 메소드 시그니처

addFilterBefore 메소드는 다음과 같은 시그니처를 가지고 있습니다

void addFilterBefore(Filter filter, Class<? extends Filter> beforeFilter)
  • filter: 추가할 필터 객체
  • beforeFilter: 추가할 필터가 어떤 필터 이전에 위치해야 하는지를 나타내는 필터 클래스

 

3. 예시

아래는 addFilterBefore 메소드를 사용한 예시입니다.

예시에서는 CustomFilter를 UsernamePasswordAuthenticationFilter 이전에 추가하고 있습니다.

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            **.addFilterBefore(new CustomFilter(), UsernamePasswordAuthenticationFilter.class)**
            // 다른 설정들...
            .authorizeRequests()
                .antMatchers("/public/**").permitAll()
                .anyRequest().authenticated()
            // 다른 설정들...
            .and()
            .formLogin()
            // 다른 설정들...
            .and()
            .logout()
            // 다른 설정들...
            .and()
            .csrf().disable();
    }
}

위의 예시에서 CustomFilter는 UsernamePasswordAuthenticationFilter 이전에 실행되도록 설정되었습니다.

 

4. 활용 사례

이 메소드는 특정한 필터를 추가하여 사용자 정의 보안 로직을 구현하거나, 기존의 필터를 수정하지 않고 새로운 필터를 추가할 때 유용합니다.

특히, 사용자 정의 로그인, 권한 부여, 혹은 다른 보안 요소를 추가하고 싶을 때 활용될 수 있습니다.

 

요약하면, addFilterBefore 메소드는 Spring Security에서 필터 체인에 사용자 정의 필터를 특정 필터 이전에 추가하는 데 사용되며, 필터 체인의 동작을 세밀하게 제어할 수 있도록 도와줍니다.

 

4️⃣ 결과물 ❗

 


Rerfence