TIL(Today I Learned)

TIL - Spring Boot에서 Filter 내 발생하는 Exception 처리

Happy._. 2024. 6. 14. 23:50

백오피스 프로젝트에서 JWT 토큰을 사용해 인증 처리 후 SecurityContext에 담기는 사용자 정보를 추출해 다른 Service 클래스에서 사용할 수 있도록 메서드를 작성했다.

그런데 이 코드는 JWT 토큰이 없는 상태에서 principal에 anonymousUser가 들어가기 때문에 UserPrincipal로 타입 캐스팅을 할 수 없다는 예외가 발생했다.

fun getMemberIdFromToken(): Long? {
    val principal = SecurityContextHolder.getContext().authentication.principal as UserPrincipal
    return principal.id
}

 

이 문제를 해결하기 위해 JwtAuthenticationFilter 클래스를 확인 해보니 JWT 토큰이 없는 경우 다음 필터로 넘어가는데 이 상태로 토큰이 필요한 요청을 보낸다면 SecurityContext에 토큰에서 추출한 정보가 담기지 않기 때문에 예외가 발생하는 것이 당연하다.

@Component
class JwtAuthenticationFilter(
    private val jwtPlugin: JwtPlugin,
) : OncePerRequestFilter() {

	// ...
    
    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        val jwt = request.getBearerToken()

        if (jwt != null) {
            jwtPlugin.validateToken(jwt)
                .onSuccess {
                
                    // ...
                  
                    SecurityContextHolder.getContext().authentication = authentication
                }

        filterChain.doFilter(request, response)
    }

	// ...
}

 

그래서 JWT 토큰이 전달되지 않는 경우에 대해 다음과 같이 else 문을 추가해 처리했다.

SecurityConfig에 이미 모든 GET 요청에 대해 인증을 요구하지 않도록 처리했지만 JwtAuthenticationFilter 클래스에 다음과 같이 GET 요청이 아니고 조건에 맞는 URI의 POST도 아니라면 토큰이 없다는 응답을 보내도록 구현했다.

해당 조건이 없으면 JWT 토큰이 없는 경우 GET 요청이라도 무조건 토큰이 없다는 응답을 보내기 때문이다. 

@Component
class JwtAuthenticationFilter(
    private val jwtPlugin: JwtPlugin,
) : OncePerRequestFilter() {

	// ...
    
    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        val jwt = request.getBearerToken()

        if (jwt != null) {
            jwtPlugin.validateToken(jwt)
                .onSuccess {
                
                    // ...
                  
                    SecurityContextHolder.getContext().authentication = authentication
                }
        } else { // 토큰이 없으면

            if (request.method != "GET") {
                if (!(request.method == "POST" && request.requestURI.startsWith("/members"))) {
                    response.status = HttpStatus.UNAUTHORIZED.value()
                    response.contentType = MediaType.APPLICATION_JSON_VALUE

                    jacksonObjectMapper().writeValue(response.writer, "No token")
                }
            }
        }
        filterChain.doFilter(request, response)
    }

	// ...
}

 

위와 같이 처리하기 전에 Filter 클래스 내에서 throw로 예외를 던지는 코드를 작성했었다.

그런데 Filter에서 작성하는 코드는 @RestControllerAdvice 어노테이션을 사용하는 GlobalExceptionHandler 클래스로 예외가 전달되지 않았다.

그 이유는 @RestControllerAdvice는 Controller에 적용되기 때문에 Filter에서 발생한 예외는 처리할 수 없다.