[Spring Security] JWT 토큰 유효성 검사 예외 처리 방법

enjoy89·2024년 4월 24일
0
post-custom-banner

웹 서비스에서 로그인 후 서비스 이용을 위해 발급받은 토큰을 API 요청 시 헤더에 담아 보냅니다. 이 과정에서 사용자의 인증 토큰이 유효한지 검사하고, 상황에 맞는 예외 처리를 어떻게 하는지에 대해 알아보겠습니다.


작업 흐름

오늘 작업 흐름은 아래와 같이 6단계로 진행됩니다.
이 포스팅에서는 JWT 토큰 유효성 검사에 관한 내용(4, 5, 6)만 다룰 것입니다.

  1. 사용자 로그인 요청
  2. JWT 발급
  3. JWT를 이용한 API 요청
  4. JwtAuthenticationFilter 동작
  5. 인증 및 권한 부여
  6. 응답

JwtAuthenticationFilter와 Security Filter Chain

요즘 많은 분들이 Spring Security를 이용해서 회원 인증 및 로그인 관리를 합니다.
Spring Security에서는 다양한 필터를 통해 요청을 처리합니다. 이 필터들은 Security Filter Chain을 구성하며, 각 필터는 특정 목적에 따라 요청을 처리합니다.

JwtAuthenticationFilter는 인증 토큰(JWT)의 유효성을 검사합니다. 이 필터를 UsernamePasswordAuthenticationFilter 앞에 추가하여, 사용자 이름과 비밀번호를 검사하기 전에 JWT의 유효성을 먼저 검사하도록 설정해야 합니다.

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                (생략)
                // UsernamePasswordAuthenticationFilter 앞 JwtAuthenticationFilter 추가
                .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
                        UsernamePasswordAuthenticationFilter.class)
                .build();
    }

이 설정에 의해, API 요청이 들어오면 JwtAuthenticationFilter가 먼저 요청을 가로채 JWT의 유효성을 검사합니다. 유효한 토큰인 경우, 요청은 다음 필터로 넘어가 처리됩니다. 유효하지 않은 경우, 예외를 던지거나 특정 처리를 수행할 수 있게 됩니다.


JWT 토큰의 유효성 검사

저는 아래와 같이 유효성 검사 결과를 담는 객체 JwtTokenValidationResult를 하나 생성해주었습니다.

@Getter
@RequiredArgsConstructor
public class JwtTokenValidationResult {

    private final boolean valid; // 토큰 유효성
    private final ErrorCode errorCode; // 토큰이 유효하지 않은 경우의 에러 코드

    public static JwtTokenValidationResult valid() {
        return new JwtTokenValidationResult(true, null);
    }

    public static JwtTokenValidationResult invalid(ErrorCode errorCode) {
        return new JwtTokenValidationResult(false, errorCode);
    }
}

그리고 JwtTokenValidationResult 객체를 토큰의 유효성 검사 메서드에 사용하여 유효한 토큰과 유효하지 않은 토큰의 결과를 담아 반환해주었습니다.

토큰이 유효한 경우에만 valid 상태값을 반환하고, 유효하지 않은 경우에는 각각의 에러 메시지와 상태값을 담은 invalid 를 넘겨주었습니다.

public JwtTokenValidationResult validateToken(String token) {
        try {
            Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(token);
            return valid(); // 유효한 토큰
        } catch (SignatureException | SecurityException | MalformedJwtException e) {
            log.info("Invalid JWT Token", e);
            return invalid(ErrorCode.INVALID_TOKEN); // 유효하지 않은 토큰 에러 반환
        } catch (ExpiredJwtException e) {
            log.info("Expired JWT Token", e);
            return invalid(ErrorCode.TOKEN_EXPIRED); // 만료된 토큰 에러 반환
        } catch (UnsupportedJwtException e) {
            log.info("Unsupported JWT Token", e);
            return invalid(ErrorCode.UNSUPPORTED_TOKEN); // 지원하지 않는 형식 토큰 에러 반환
        } catch (IllegalArgumentException e) {
            log.info("JWT claims string is empty.", e);
            return invalid(ErrorCode.NOT_FOUND_TOKEN); // 토큰의 클레임이 비어 있는 경우 에러 반환
        }
    }

그런 다음 앞에서 설정했던 JwtAuthenticationFilterdoFilterInternal() 메서드를 아래와 같이 오버라이딩하여 토큰이 유효한 경우에만 사용자 인증 정보를 설정하고, 유효하지 않은 경우에는 다음 처리를 중단하도록 설정했습니다.

토큰 유효성 검사에 대한 예외 메시지는 sendErrorResponse() 메서드를 통해 Response 하도록 구현했습니다.

@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtTokenProvider jwtTokenProvider;

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

        String token = resolveToken(request);

        // 토큰이 유효한 경우에만 인증 정보를 설정
        if (token != null) {
            JwtTokenValidationResult validationResult = jwtTokenProvider.validateToken(token);
            if (validationResult.isValid()) {
                Authentication authentication = jwtTokenProvider.getAuthentication(token);
                SecurityContextHolder.getContext().setAuthentication(authentication);
                log.info("토큰이 유효합니다.");
            } else {
                sendErrorResponse(response, validationResult.getErrorCode());
                return; // 유효하지 않은 경우, 처리를 중단
            }
        }

        filterChain.doFilter(request, response);
    }
    
    
    /**
     * 유효하지 않은 토큰에 대한 에러 반환
     */
    private void sendErrorResponse(HttpServletResponse response, ErrorCode errorCode) throws IOException {
        Response<Void> errorResponse = Response.fail(errorCode.getStatus(), errorCode.getMessage());
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(errorCode.getStatus().value()); // ErrorCode에 따른 적절한 HTTP 상태 코드 설정
        response.getWriter().print(new ObjectMapper().writeValueAsString(errorResponse)); // 재사용 가능한 ObjectMapper 인스턴스 사용
    }
}

Response Body

{
    "code": 401,
    "message": "유효하지 않은 토큰입니다."
}

정리

JwtAuthenticationFilter가 Spring Security Filter Chain 내에서 어떻게 동작하는지 알아보았습니다.
JWT 토큰의 유효성을 검사하는 과정에서 어떤 예외들이 발생할 수 있는지, 그리고 이러한 예외들을 어떻게 처리하는지에 대해 자세히 살펴보았습니다.

profile
Backend Developer 💻 😺
post-custom-banner

0개의 댓글