ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • java.lang.ClassNotFoundException: javax.xml.bind.DatatypeConverter 에러
    Spring/기타 2024. 11. 27. 20:12

    ▤ 목차

       

       

       

      ✔ 에러 발생 과정

      ✏️ JWT 토큰 생성 과정중 발생

      카카오 로그인 API을 진행 중 오류가 발생했다.

       

      ERROR 50816---[..]: Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed: java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter] with root cause

       

      발생한 오류를 보면 

      " DatatypeConverter 클래스가 클래스 경로에서 찾을 수 없다 " 고 했다.

       

      ✔️ 해결

      같은 에러가 뜬 다른 블로그를 찾아보니 Java8 이후 버전에서는 해당 클래스를 찾을 수 없기 때문이라고 한다.

       

      우선 해결을 하고나서 공부하기로 한다.

      jwt 의존성을 확인하기 위해 build.gradle 파일에 들어갔다.

       

      지금 내가 사용하고 있던 jjwt 의 버전은 9버전이다. 

      그래서 최신버전인 11버전으로 업그레이드 해봤다.

      그리고 다른 필요한 impl.jar 등등 의존성도 함께 해줘야한다.

      (하.. 기본 jjwt-api 11버전만 설정했다가 런타임에 500에러나서 log 엄청 찍고 찾아내니 impl 의존성 추가 안해서였다..)

      	implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
      	runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
      	runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' // JSON 처리에 필요한 모듈

      ** jjwt-impl과 jjwt-jackson은 jjwt-api의 동작을 위해 반드시 필요한 의존성**

       

      또 다른 방법도 있었다.

      JAXB와 관련된 의존성을 직접 추가하는 방법도 시도해봤다.

      implementation 'io.jsonwebtoken:jjwt:0.9.1'
      implementation 'com.sun.xml.bind:jaxb-impl:4.0.1'
      implementation 'com.sun.xml.bind:jaxb-core:4.0.1'
      implementation 'javax.xml.bind:jaxb-api:2.4.0-b180830.0359'

       

      나는 다른 에러가 떴다.(아래에서 연속해서 정리) 그러나 위의 에러는 의존성때문에 문제였기에

      해당 에러만 났다면 정상작동할 것이다. 

       

       

      👏  의존성 문제

      해당 에러는 의존성을 둘중 하나로 바꿔주면 보통은 해결되는 것같다.
      https://5g-0.tistory.com/11


      나는 두가지 방법중에 jjwt 11버전으로 선택했다.

      왜냐하면 11버전에는 javax.xml.bind.DatatypeConverter 관련 문제를 해결하고, 최신 기능과 안정성을 제공하기 때문이다.

      -- 해당 오류의 이유는 버전의 문제 였는데 --

      더보기

      javax.xml.bind.DatatypeConverter 클래스를 찾을 수 없는 이유는 JAXB (Java Architecture for XML Binding)가 JDK 9 이상에서 모듈화되어 기본적으로 제공되지 않기 때문이다.

      즉, javax.xml.bind.DatatypeConverter는 Java 8에서는 기본적으로 제공되었지만, Java 9 이상에서는 JAXB API모듈화되었고 기본 JDK에서 제거됐기때문이다.

       

       

       


       

       

       

      ✔ 시큐리티 필터를 적용했다가 불편해서

      의존성 버전을 바꾸기도 했고 JAXB 관련 의존성을 직접 추가해보기도 했는데 화면에 다른 오류메시지가 안뜨고 500에러라고만 떴다.

      빠르게 서버 터미널을 보니 Spring Security의 필터 체인 관련 설정에서 오류가 났다.

       

      개발단계에서 불편해서 필터 부분을 주석처리했는데, 관련해서 문제가 생긴거라는 추측을 하며 에러를 검색했다.

      그런데, 에러 메세지가 바뀌는 것이 아닌가?

      뭐가 다른지 비교하기 위해 오류만 따로 복붙해봤다.

       

      🤔 jjwt 11버전 시

      SpringBootWebSecurityConfiguration.SecurityFilterChainConfiguration matched:
            - AllNestedConditions 2 matched 0 did not; NestedCondition on DefaultWebSecurityCondition.Beans @ConditionalOnMissingBean (types: org.springframework.security.web.SecurityFilterChain; SearchStrategy: all) did not find any beans; NestedCondition on DefaultWebSecurityCondition.Classes @ConditionalOnClass found required classes 'org.springframework.security.web.SecurityFilterChain', 'org.springframework.security.config.annotation.web.builders.HttpSecurity' (DefaultWebSecurityCondition)
      
         SpringBootWebSecurityConfiguration.WebSecurityEnablerConfiguration matched:
            - @ConditionalOnClass found required class 'org.springframework.security.config.annotation.web.configuration.EnableWebSecurity' (OnClassCondition)
            - @ConditionalOnMissingBean (names: springSecurityFilterChain; SearchStrategy: all) did not find any beans (OnBeanCondition)

      🤔 JAXB와 관련된 의존성 직접 추가 시

      SpringBootWebSecurityConfiguration.SecurityFilterChainConfiguration:
            Did not match:
               - AllNestedConditions 1 matched 1 did not; NestedCondition on DefaultWebSecurityCondition.Beans @ConditionalOnMissingBean (types: org.springframework.security.web.SecurityFilterChain; SearchStrategy: all) found beans of type 'org.springframework.security.web.SecurityFilterChain' securityFilterChain; NestedCondition on DefaultWebSecurityCondition.Classes @ConditionalOnClass found required classes 'org.springframework.security.web.SecurityFilterChain', 'org.springframework.security.config.annotation.web.builders.HttpSecurity' (DefaultWebSecurityCondition)
      
         SpringBootWebSecurityConfiguration.WebSecurityEnablerConfiguration:
            Did not match:
               - @ConditionalOnMissingBean (names: springSecurityFilterChain; SearchStrategy: all) found beans named springSecurityFilterChain (OnBeanCondition)
            Matched:
               - @ConditionalOnClass found required class 'org.springframework.security.config.annotation.web.configuration.EnableWebSecurity' (OnClassCondition)

       

       

      ✏️메시지 분석

      🔸 SpringBootWebSecurityConfiguration.SecurityFilterChainConfiguration:

      SecurityFilterChain 설정을 위한 조건을 모두 만족하지 못했다고 한다.

      이게 스프링 시큐리티 설정이 불완전하거나 누락되면 발생하는 에러라고 한다.

       

      @ConditionalOnMissingBean 이 해당 ` SecurityFilterChain `빈을 찾을 수 없다는 의미였다.

       

       

      🔸 SpringBootWebSecurityConfiguration.WebSecurityEnablerConfiguration:

      @ConditionalOnMissingBean에 의해 springSecurityFilterChain이라는 빈이 이미 존재한다는 메시지다.

       

       

       

      👻 일단 주석을 풀어봐야겠다..

      springSecurityFilterChain 빈 충돌: @ConditionalOnMissingBean 조건에 의해 springSecurityFilterChain 빈이 이미 존재하는 경우에 추가적인 설정이 필요한데, 설정이 없거나 불완전할 경우 문제가 생기기 때문이다. 

      결국은 불완전하다가 맞는것같으니 주석을 풀어보기로 했다.

      위의 주석을 모두 풀어줬다.

       

      1.

       

       


       

       

       

      ✔ `requestMatchers("/**").permitAll()` 코드 동작

      requestMatchers("/**").permitAll()은 모든 경로를 허용하도록 설정한다.

      이 말은, /api/**, /users/** 등 모든 경로에 대해 인증을 요구하지 않겠다는 의미이다.

       

      위에서 말했듯 필터 코드를 작성한 후 개발단계에서 권한 관련 에러가 나는게 힘들어서 어떤 경로이든 허용하도록 하고 싶었다. 하지만 /** 경로가 먹히지 않았다.

       

      🤔전체를 허용시켰는데 왜 적용이 안되는거지?

      Spring Security의 필터 체인 순서와 관련이 있다.

       

      JwtAuthenticationFilterSecurityFilterChain

      위에 캡쳐본에서도 잠깐 보였지만 내 코드는 

      `JwtAuthenticationFilter`는 SecurityConfig에서 설정한 것처럼 UsernamePasswordAuthenticationFilter 앞에 추가되어 있었다.

       

      JwtAuthenticationFilter 필터는 모든 요청을 가로채서 요청 헤더에 있는 JWT 토큰을 검증한다.

      이때 토큰을 검증하고 유효하지 않으면 401에러를 반환한다.

       

      ❓ 질문이 생겼다.


      체인은 차례로 실행되는 것으로 알고 있다.

      그럼 .requestMatchers("/**").permitAll() 이 설정이 뒤의 설정보다 우위에 있어야하는게 아닌가?

       

      authorizeHttpRequests에서 설정한 경로 권한에 대한 규칙은 차례대로 적용된다.

      그래서 requestMatchers("/**").permitAll()이 뒤에 있는 다른 규칙보다 우선 적용되어야 하는 것이 맞다.

      하지만 JwtAuthenticationFilter가 요청을 차단하거나 토큰을 검사하는 방식 때문에 이런 일이 발생한것.

       

      즉, JwtAuthenticationFilter가 먼저 요청을 가로채기 때문이다.

       

      코드를 보면 UsernamePasswordAuthenticationFilter(폼 로그인 인증) 보다 JwtAuthenticationFilter(jwt 인증)가 먼저 실행되도록 되어있다

      이 두 인증 방식은 서로 독립적으로 동작이 가능하다.

       

       

      👏 만약 필터에서 제외하고 싶다면

      인증이 필요없는 경로를 예외처리해줘야한다.

      [예시 코드]

      public class JwtAuthenticationFilter extends OncePerRequestFilter {
      
          private final JwtTokenProvider jwtTokenProvider;
      
          public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {
              this.jwtTokenProvider = jwtTokenProvider;
          }
      
          @Override
          protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
              // 인증이 필요하지 않은 URL은 필터를 거치지 않도록 예외 처리
              String requestURI = request.getRequestURI();
              if (requestURI.equals("인증이 필요하지 않은 URL")) {
                  filterChain.doFilter(request, response);  // 인증 없이 필터를 통과
                  return;
              }
      
              // JWT 토큰 검증 로직
              String token = jwtTokenProvider.resolveToken(request);
              if (token != null && jwtTokenProvider.validateToken(token)) {
                  Authentication authentication = jwtTokenProvider.getAuthentication(token);
                  SecurityContextHolder.getContext().setAuthentication(authentication);
              }
      
              filterChain.doFilter(request, response);
          }
      }

       

       


       

       

      ✔jwt의 전반적인 흐름

       

      🙊Spring Security 필터 체인 흐름

      Filter Chain을 통해 요청을 처리한다.

      각 필터는 특정 역할을 담당하며 요청이 들어오면 차례로 필터를 통과하게 된다.

      SecurityFilterChain에 설정된 대로 순차적으로 실행


      🙉 JwtAuthenticationFilter

      JwtAuthenticationFilter는 JWT 토큰을 기반으로 인증을 처리하는 필터를 말한다.

      JwtAuthenticationFilter는 주로 Bearer 토큰을 사용하여 인증을 처리한다.

      1. 요청이 들어오면, HTTP 헤더에서 authorization 필드에 담긴 jwt 토큰을 추출한다.

      2. 추출한 jwt 토큰을 검증하여 유효하면 해당 토큰에서 인증 정보를 Authentication 객체로 변환하여 SecurityContextHolder에 저장한다.

      3. 인증이 완료되면 요청을 처리하고, 인증이 실패하면 401 Unauthorized 오류를 반환합니다.

       

      🙉 UsernamePasswordAuthenticationFilter

      UsernamePasswordAuthenticationFilter는 사용자 이름과 비밀번호를 기반으로 인증을 처리하는 필터를 말한다.

      일반적으로 로그인 폼에 제공된 사용자 이름과 비밀번호를 사용하여 인증을 시도한다.

       

      1. 로그인 요청이 들어온다.

      2. UsernamePasswordAuthenticationFilter가 요청을 가로챈다.

      3. 요청에서 사용자 이름과 비밀번호를 추출해서 AuthenticationManager를 사용해 인증 시도

      4. 인증이 성공하면 SecurityContextHolder에 Authentication 객체를 저장해 인증 완료 상태가 된다.

      5. 인증이 실패하면 401 Unauthorized 오류를 반환한다.

       

       

      🙊 전반적인 흐름

      1) HTTP 요청이 들어온다.

      • 사용자가 로그인 화면에서 로그인 요청을 보낸다.
        이 요청은 `POST /login`과 같은 로그인 요청을 말한다.

      2) UsernamePasswordAuthenticationFilter 실행

      • UsernamePasswordAuthenticationFilter가 요청을 가로채고, 사용자 이름과 비밀번호를 **AuthenticationManager**를 사용해 인증을 시도한다.
      • 인증이 성공하면 `SecurityContextHolder`에 인증 정보가 저장되고, 요청은 계속해서 후속 필터를 거친다.
      • 인증이 실패하면 401 Unauthorized 응답이 반환된다.

      3) JWT 인증이 필요한 요청

      • 로그인 후 서버에서 발급한 JWT 토큰을 Authorization 헤더에 담아서 요청한다.
      • 예를 들어, 사용자가 `GET /api/protected`와 같은 보호된 경로에 접근하려 할 때 Authorization: Bearer <JWT> 헤더를 포함하여 요청한다.

      4) JwtAuthenticationFilter 실행

      • JwtAuthenticationFilter는 Authorization 헤더에서 JWT 토큰을 추출하고, 이를 검증한다.
      • JWT가 유효하다면, 토큰에서 사용자 정보를 추출하여 Authentication 객체를 생성하고, `SecurityContextHolder`에 저장한다.
      • 인증이 완료되면 요청이 계속 처리되고, 인증이 실패하면 401 Unauthorized 응답이 반환된다.

      5) 후속 필터 실행

      • 인증이 완료되면 요청은 계속 후속 필터들, 예를 들어 SecurityContextPersistenceFilter(세션 관리), ExceptionTranslationFilter(예외 처리), FilterSecurityInterceptor(접근 권한 검사) 등을 거친다.
      • 이들 필터는 보통 세션 관리, 예외 처리, 접근 권한 검사 등을 처리한다.

      6) 최종 응답 반환

      • 요청에 대한 인증과 권한 처리가 모두 끝나면, 요청에 대한 최종 응답이 반환된다.
        예를 들어, 보호된 자원에 접근 성공 시 200 OK와 함께 리소스가 반환.
      • 인증이 실패한 경우 401 Unauthorized 오류가 반환.

       

      👏 SecurityContextHolder의 역할

      SecurityContextHolder는 현재 사용자 인증 정보(Authentication 객체)를 저장하고 관리하는 장소이다.

      Spring Security는 이 객체를 사용하여 인증된 사용자의 정보에 접근하거나, 후속 필터에서 인증 상태를 확인할 수 있도록 한다.

      이 정보는 일반적으로 SecurityContext에 저장되며, 보통 SecurityContextPersistenceFilter가 이 정보를 HttpSession에 저장하거나, JWT 기반 인증에서는 세션을 사용하지 않고 직접 SecurityContext에 인증 정보를 보관한다.

       


       

       

      😊정리

      ✏️ HttpSecurity에서 경로 예외 처리
      requestMatchers("/api/users/kakao/loginPage", ...) 를 인증 없이 접근할 수 있는 경로를 명시적으로 정의

       

      ✏️ JwtAuthenticationFilter에서 명시적인 경로 예외 처리 

      doFilterInternal 메서드 내에서 요청 URI를 확인하고, 특정 경로에 대해서만 JWT 검증을 건너뛰도록 처리

       

       JwtAuthenticationFilter 에서 예외처리를 추가하는 이유는 필터가 모든 요청에 대해 JWT검증을 하도록 설계되어 있기 때문이다.

       

       

      개인 공부중인데 틀린 부분있으면 댓글로 알려주세요!

    Designed by Tistory.