베어_
TechBear
베어_
전체 방문자
오늘
어제
  • 분류 전체보기 (336)
    • Spring (33)
      • 개념 (13)
      • Security (5)
      • 실습 (1)
      • 토비 스프링 (11)
    • JPA (6)
    • 프로젝트 기록 (24)
    • DB (13)
    • JAVA (18)
    • 알고리즘 (50)
      • 유형정리 (8)
      • Baekjoon (21)
      • LeetCode (18)
    • 디자인패턴 (0)
    • 개발서적 (79)
      • Effective Java (78)
      • 객체지향의 사실과 오해 (1)
    • 독후감 (4)
    • 보안 (2)
    • 운영체제(OS) (53)
      • 공룡책 (53)
    • 컴퓨터 네트워크 (28)
      • 컴퓨터 네트워크 하향식 접근 (23)
    • 자료구조 (1)
    • DevOps (2)
    • 앱 개발 (20)
      • 안드로이드 스튜디오 (20)

블로그 메뉴

    공지사항

    인기 글

    태그

    • 스레드
    • 스프링
    • C++
    • 코드업
    • Spring
    • 스프링시큐리티
    • 함수형인터페이스
    • jpa
    • 운영체제
    • 토비스프링
    • 알고리즘
    • 이펙티브자바
    • 자바
    • dfs
    • java
    • 자바8
    • leetcode
    • BFS
    • 백준
    • 데이터베이스

    최근 댓글

    최근 글

    티스토리

    hELLO · Designed By 정상우.
    베어_

    TechBear

    프로젝트 기록

    [Debug] JWT토큰 디버그

    2023. 5. 25. 23:52

    [문제 상황]

       어제까지만 해도 잘 되던 my-page 기능이 제대로 동작하지 않는 문제가 있었다. 

    my-page의 프로세스는 다음과 같다.

    1. my-page에 접속한다.
    2. 토큰이 있는지 확인한다. 
      1. 만약 토큰이 없으면 로그인 페이지로 리다이렉트 한다.
      2. 토큰이 있으면 백엔드 서버로 이를 전송한다.
    3. 백엔드 서버에서 응답을 받는다.
      1. 만약 토큰이 유효하지 않으면 로그인 페이지로 리다이렉트 한다. (문제 상황)
      2. 토큰이 유효하면 사용자 정보를 JSON 형태로 리턴 받는다.

    위와 같이 현재 진행하고 있는 프로젝트 'VOCA'는 JWT토큰을 베이스로 구성되어 있다. 현재 문제는 3-1로 토큰이 만료되거나 유효하지 않았을 때 리다이렉트 기능이 제대로 동작하지 않는다는 것이다.

    [Debug]

       서버의 에러 로그를 확인해보니 예상과는 달리 InvalidHeaderException을 리턴하고 있었다. 

    [ERROR] [ERROR] 2023-05-25 21:00:44 [http-nio-8088-exec-8] [GeneralExceptionHandler:63] - InvalidHeaderException vanille.vocabe.global.exception.InvalidHeaderException: 잘못된 헤더 토큰입니다

    서버의 코드는 다음과 같이 작성되어 있었다.

    // 1. http 헤더로부터 토큰 정보를 추출한다.
    final String token = request.getHeader(HttpHeaders.AUTHORIZATION);
    
    // 1-1. 만약 헤더에 토큰 정보가 없으면 토큰이 없다는 예외를 던진다.
    if(!StringUtils.hasText(token)) {
        throw new NotFoundException(ErrorCode.NOT_FOUND_TOKEN);
    }
    
    User userDetails;
    try {
        // 2. 토큰에서 사용자 정보를 얻어온다.
        String username = JwtTokenUtils.getUsername(token, secretKey);
        userDetails = userService.findUserByUsername(username);
    
        if (!JwtTokenUtils.validate(token, userDetails.getUsername(), secretKey)) {
            log.error("Token is not valid {}", token);
            throw new ExpiredTokenException(ErrorCode.EXPIRED_TOKEN);
        }
    } catch (Exception e) {
        throw new InvalidHeaderException(ErrorCode.INVALID_TOKEN);
    }

    디버깅 모드를 통해 자세히 살펴보니 ExpiredToken 예외가 발생하여 getBody()를 얻지 못하여 catch문에서 예외로 걸러지고 있었다.

     

    public class JwtTokenUtils {
        public static Boolean validate(String token, String userName, String key) {
            String usernameByToken = getUsername(token, key);
            return usernameByToken.equals(userName) && !isTokenExpired(token, key);
        }
    
        public static String getUsername(String token, String key) {
            return extractAllClaims(token, key).get("username", String.class);
        }
    
        public static Claims extractAllClaims(String token, String key) {
            return Jwts.parserBuilder()
                    .setSigningKey(getSigningKey(key))
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
        }
    }

    validate() 코드를 보면 isTokenExpired() 메서드가 있는데 이 메서드 때문에 extractAllClaims() 메서드를 호출할 때는 토큰 만료와 관련된 에러는 안 던진다고 생각한 것이다. 

    코드는 다음과 같이 수정했다. ExpiredJwtException이 발생하면 토큰 에러를 던지고 나머지 예외는 Exception으로 잡고 에러 로그를 남기는 식으로 코드를 변경했다.

    try {
        String username = JwtTokenUtils.getUsername(token, secretKey);
        userDetails = userService.findUserByUsername(username);
    
        JwtTokenUtils.validate(token, userDetails.getUsername(), secretKey);
    } catch (ExpiredJwtException e) {
        throw new ExpiredTokenException(ErrorCode.EXPIRED_TOKEN);
    } catch (Exception e) {
        log.error("Error : " + e.getMessage() + ", Cause : " + e.getCause());
        throw new InvalidHeaderException(ErrorCode.INVALID_TOKEN);
    }

     

    테스트 코드도 추가하였다. 

    @DisplayName("[실패] 토큰이 만료되었을 때")
    @Test
    void authInterceptorFailWithExpiredToken() throws Exception {
        User user = UserFixture.getVerifiedUser("kim");
        userRepository.save(user);
    
        final String token;
        try {
            token = JwtTokenUtils.generateAccessToken("kim", 1000L, key);
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        mockMvc.perform(post("/api/v1/words")
                        .header(HttpHeaders.AUTHORIZATION, token)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(WordFixture.get(user)))
                )
                .andExpect(jsonPath("statusCode").value(ErrorCode.EXPIRED_TOKEN.getHttpStatus().toString()))
                .andDo(print());
    }
    
    @DisplayName("[실패] 토큰 유효하지 않을 때")
    @Test
    void authInterceptorFailWithInvalidToken() throws Exception {
        User user = UserFixture.getVerifiedUser("kim");
        userRepository.save(user);
        mockMvc.perform(post("/api/v1/words")
                        .header(HttpHeaders.AUTHORIZATION, "Bearer " + "wrongtoken.dwfwdf.wdfxcv")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(WordFixture.get(user)))
                )
                .andExpect(jsonPath("statusCode").value(ErrorCode.INVALID_TOKEN.getHttpStatus().toString()))
                .andDo(print());
    }

     

    이펙티브 자바에서 예외는 가장 상세한 예외를 던지는 것이 좋다 하여 catch(DecodeException e)를 추가할려고 하였는데 추가 로직이 필요없고, 로그 에러를 남기기만해도 충분할 것 같다는 생각이 들어 생략하였다.

    저작자표시 비영리 변경금지 (새창열림)
      '프로젝트 기록' 카테고리의 다른 글
      • OSIV 필터와 지연로딩 에러
      • 커스텀 예외 처리와 NestedServletExceeption
      • [리팩토링] Pageable을 이용한 페이징
      • [회고록] 서비스 오픈 회고
      베어_
      베어_
      Today I learned | 문제를 해결하는 개발자

      티스토리툴바