🌿SPRING/🌱연습[SPRING]

[SPRING] JWT Exception

디카페인라떼 2022. 10. 6. 15:44

왜 따로 처리해주어야 할까?

JWT 토큰의 예외처리는 이전에 만들어 둔 전역 예외처리에 걸리지 않는다.

전역 예외처리는 Spring의 영역이지만 JWT filter는 spring 이전의 이야기이다 

 

👉 즉 DispatcherServlet까지 들어오지도 못하고 필터단에서 예외처리되어 나가버린다

👉JWT 필터는 이중에 하나로 여기서 예외처리가 되어 Response되므로 따로 처리를 해주어야 한다!

 


어떻게 처리해주면 될까?
  • 필터에서 발생하는 예외를 try-catch
  •  AuthenticationEntryPoint에서 각각 예외를 구분하여 예외 처리를 한다

 

 

  • JWT 필터
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws IOException, ServletException {

        try {//전체를 try
            .
            .
            String jwt = resolveToken(request);

            if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
                Claims claims;
                try {
                    claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(jwt)
                        .getBody();
                } catch (ExpiredJwtException e) {
                    claims = e.getClaims();
                    request.setAttribute("exception", ErrorCode.EXPIRED_TOKEN);
                }

                if (claims.getExpiration().toInstant().toEpochMilli() < Instant.now()
                    .toEpochMilli()) {
                    response.setContentType("application/json;charset=UTF-8");
                    response.getWriter().println(
                        new ObjectMapper().writeValueAsString(
                            ResponseDto.fail(ErrorCode.BAD_TOKEN_REQUEST)));
                    response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                }

                String subject = claims.getSubject();
                Collection<? extends GrantedAuthority> authorities =
                    Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toList());

                UserDetails principal = userDetailsService.loadUserByUsername(subject);

                Authentication authentication = new UsernamePasswordAuthenticationToken(principal,
                    jwt, authorities);
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }catch (CustomErrorException e) { //catch로 감싸줌
            request.setAttribute("exception", e.getErrorCode());
        }

        filterChain.doFilter(request, response);
    }

👉 토큰 유효성 검증이 TokenProvider에서 이루어지므로 필터 단에서 바로 throw 처리를 할수 가 없어서 

일단 전체를 감싸서 try-catch 처리를 해주었다.

 

  • TokenProvider
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (SecurityException | MalformedJwtException e) {
//            log.info("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
            throw new CustomErrorException(ErrorCode.INVALID_TOKEN);
        } catch (ExpiredJwtException e) {
//            log.info("Expired JWT token, 만료된 JWT token 입니다.");
            throw new CustomErrorException(ErrorCode.EXPIRED_TOKEN);
        } catch (UnsupportedJwtException e) {
//            log.info("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
            throw new CustomErrorException(ErrorCode.UNSUPPORTED_TOKEN);
        } catch (IllegalArgumentException e) {
//            log.info("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
            throw new CustomErrorException(ErrorCode.WRONG_TOKEN);
        }
    }

👉 따로 JWT Exception class를 만들진 않고 기존의 CustomException을 활용하였다.

  • CustomAuthenticationEntryPoint
@Slf4j
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

  @Override
  public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
    Object exception = request.getAttribute("exception");

    if(exception instanceof ErrorCode){
      ErrorCode errorCode = (ErrorCode) exception;
      setResponse(response, errorCode);

      return;
    }

    response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
  }

  //한글 출력을 위해 getWriter() 사용 Spring을 거치는 게 아니어서 한글 처리를 위해선 꼭 해줘야함
  private void setResponse(HttpServletResponse response, ErrorCode exceptionCode) throws IOException {
    log.error("token error : {}, {}",exceptionCode.getCode(), exceptionCode.getMessage());

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

    JSONObject responseJson = new JSONObject();
    responseJson.put("message", exceptionCode.getMessage());
    responseJson.put("code", exceptionCode.getCode());

    response.getWriter().print(responseJson);
  }
}

👉이부분은 이해가 잘안되서 더 정리하기 

 

👉Attribute의 반환값은 Object로써, 반환 받은 Object Exception을

casting을 하기 전에 조건절을 사용하여  insteadOf 비교연산자로 Object exception과 Error code가 같은 인스턴스인지 비교한 뒤에

true 일때 형변환하여 set Response를 내보내고

false로 나오게되면 HttpServletResponse에 인증오류로 반환되도록 하였다 

 

🤯insteadOf 연산자?

더보기

https://www.baeldung.com/java-instanceof

 

Java instanceof Operator | Baeldung

Learn about the instanceof operator in Java

www.baeldung.com

👉 인스턴스 비교 연산자

👉 is-a 관계의 원칙에 따라서 작동함

✔ 클래스의 상속 또는 인터체이스 구현을 기반으로 함 
=> 비교 대상과 비교대상 유형 사이에 관계가 없는 경우 사용 불가 
✔ Object 유형과 함께 사용하면 항상 true 

@Test
public void givenWhenTypeIsOfObjectType_thenReturnTrue() {
    Thread thread = new Thread();
    Assert.assertTrue(thread instanceof Object);
}

✔ 객체가 null 일때는 항상 false => null 검사 필요하지 않음.

✔제네릭과의 사용 

=> 삭제된  제네릭 유형과는 사용 불가

=> 제네릭 유형 매개변수는 구체화되지 않으므로 사용 불가

=> 즉 자바에서 구체화된 유형과만 사용가능

  • int 와 같은 기본 유형
  • String  또는 Random 과 같은 제네릭이 아닌 클래스 및 인터페이스
  • Set<?> 또는 Map<?, ?> 와 같이 모든 유형이 무제한 와일드카드인 제네릭 유형
  • List 또는 HashMap 과 같은 원시 유형
  • String[], List[]  또는 Map<?, ?>[] 와 같은 다른 구체화 가능한 유형의 배열
  • 그러나 List<?>은 가능 
if (collection instanceof List<?>) {
    // do something
}
  • 시큐리티 설정 추가
@Override
    protected void configure(HttpSecurity http) throws Exception {
        Filter filter = new JwtAuthenticationFilter(authenticationManager(), jwtUtil());

        http
                .httpBasic().disable()
                .cors().and()
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
                .antMatchers("/*/*/signin", "/*/*/users/**", "/exception/**").permitAll()
                .antMatchers(HttpMethod.GET, "/api/v1/terms").permitAll()
                .anyRequest().hasRole("SUB")
                .and()
                .exceptionHandling()//이것과 같이 
                .authenticationEntryPoint(new CustomAuthenticationEntryPoint())//두개를 추가
                .and().addFilter(filter);
    }

 

👉시큐리티 설정 안에 CustomAuthenticationEntryPoint를 포함시켜 주어야 필터를 통과하면서 이부분도 같이 거치게된다.

 


이것과 다르게 권한에서 예외가 나오는 게 있는데 이부분은 현재 하는 프로젝트에서 Admin 권한이 없어서 예외를 구별할 필요가 없어서 추가하지 않았다. 그래도 나중에 필요하다면  참고하기 

더보기