Spring boot에서 JWT 구현(3)

2022. 2. 20. 16:59·Backend/Spring

이전 글

2022.02.19 - [Backend/Spring] - Spring boot에서 JWT 구현(1)

2022.02.20 - [Backend/Spring] - Spring boot에서 JWT 구현(2)

 

 

바로 전 글에서 Access token을 발급하고 Refresh token을 Redis에 저장하는 과정까지 진행해보았습니다❗

이번 포스팅에서는 Access token이 만료되었을 때 재발급하는 과정을 프론트엔드 코드와 같이 조금 살펴보겠습니다.

 

 

 

Access Token 만료


@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    try {
        String path = request.getServletPath();

        if(path.startsWith("/api/auth/reissue")) {
            filterChain.doFilter(request, response);
        } else {
            String accessToken = tokenProvider.resolveAccessToken(request);
            boolean isTokenValid = tokenProvider.validateToken(accessToken, request);

            if (StringUtils.hasText(accessToken) && isTokenValid) {
                this.setAuthentication(accessToken);
            }

            filterChain.doFilter(request, response);
        }

    } catch (ExpiredJwtException e) {
        ResponseVO responseVO = ResponseVO.builder()
                .status(ErrorCode.JWT_ACCESS_TOKEN_EXPIRED.getStatus())
                .message(ErrorCode.JWT_ACCESS_TOKEN_EXPIRED.getMessage())
                .code(ErrorCode.JWT_ACCESS_TOKEN_EXPIRED.getCode())
                .build();

        response.setStatus(ErrorCode.JWT_ACCESS_TOKEN_EXPIRED.getStatus().value());
        response.getWriter().write(new ObjectMapper().writeValueAsString(responseVO));
        response.getWriter().flush();
    }
}
  • 전에 작성했던 JwtAuthenticationFilter 코드의 일부입니다.
  • 보시면 ExpiredJwtException 처리를 하고 있는데 토큰 만료 시에 나오는 Exception으로 저의 경우 JWT_ACCESS_TOKEN_EXPIRED라는 이름으로 Custom 401 에러를 내려주었습니다

 

{
    "status":"UNAUTHORIZED",
    "message":"Access token has expired",
    "response":null,
    "code":"TOKEN-0001"
}
  • 만료된 Access 토큰을 가지고 아무 API를 호출하고 받은 return 값입니다.
  • 저는 Access 토큰 만료 에러와 Refresh 토큰 만료 에러를 구분하여 내려주었습니다.
  • 재발급 API 요청을 어떤 경우에 보내야 하는지 구분하기 위함입니다.

 

 

Axios Interceptor 사용


🛠 React.js 17.0.1, Axios 0.24.0을 사용하였습니다 🛠

 

axios.interceptors.request.use(function(config) {
    const token = localStorage.getItem('token')
    if(token) {
        config.headers.Authorization = process.env.REACT_APP_TOKEN_PREFIX + ' ' + token
    }
    return config;
})

axios.interceptors.response.use(
    success => success,
    async(error) => {
        const errorCode = error.response.data.code

        if(errorCode === 'TOKEN-0001') {
            const originRequest = error.config

            await axios.post('/api/auth/reissue')
            .then(result => {
                localStorage.setItem('token', result.data.response.accessToken)
                window.location.reload()
            })
            .catch(error => {
                localStorage.removeItem('token')
            })
            return Promise.reject(error)
        }
    }
)
  • axios interceptor란 axios를 이용한 API를 요청할 때 모두 가로채 와서 해당 코드를 실행하는 코드입니다.
  • axios.interceptors.response는 모든 API가 응답이 내려올 때, axios.interceptors.request는 모든 API를 보낼 때 실행되는 코드를 의미합니다😀
  • 위의 코드를 설명하자면 request는 모든 API를 보내기 전에 Header 값으로 Authorization이라는 이름으로 'Bearer <token>'을 넣어줍니다.
  • 모든 API의 응답에서 에러가 발생하는 경우 ErrorCode가 'TOKEN-0001'일 때 즉, Access token이 만료가 되었다는 에러일 때 토큰을 재발급하는 API를 호출합니다.
  • 성공적으로 API가 요청이 되면 localstorage에 저장을 하고 reload를 통해서 원래 실행하려던 API를 다시 요청합니다.
  • 만일 여기서 에러가 나는 경우 Refresh Token이 만료가 되었다고 생각하고 storage의 토큰 값을 삭제해 아예 로그아웃을 시켜버립니다.

 

 

Access Token 재발급 API


@PostMapping("/reissue")
public ResponseEntity<ResponseVO> reissue(HttpServletRequest request, HttpServletResponse response) throws IOException {
    MemberDTO memberDTO = authService.reissue(request, response);
    return ResponseEntity
            .status(HttpStatus.OK)
            .body(ResponseVO.builder()
                .status(HttpStatus.OK)
                .response(memberDTO)
                .build());
}

 

@Override
public MemberDTO reissue(HttpServletRequest request, HttpServletResponse response) throws IOException {
    try {
        String token = tokenProvider.resolveAccessToken(request);
        // 만료된 Access Token을 디코딩하여 Payload 값을 가져옴
        HashMap<String, String> payloadMap = JwtUtil.getPayloadByToken(token);
        String email = payloadMap.get("sub");

        // Redis에 저장된 Refresh Token을 찾고 만일 없다면 401 에러를 내려줍니다
        Optional<Token> refreshToken = redisRepository.findById(email);
        refreshToken.orElseThrow(
            () -> new CustomException(ErrorCode.UNAUTHORIZED)
        );

        // Refresh Token이 만료가 된 토큰인지 확인합니다
        boolean isTokenValid = tokenProvider.validateToken(refreshToken.get().getValue(), request);

        // Refresh Token이 만료가 되지 않은 경우
        if(isTokenValid) {
            Optional<Member> member = userRepository.findById(email);

            if(member.isPresent()) {
                // Access Token과 Refresh Token을 둘 다 새로 발급하여 Refresh Token은 새로 Redis에 저장
                Token newAccessToken = tokenProvider.createAccessToken(member.get());
                Token newRefreshToken = tokenProvider.createRefreshToken(member.get());

                tokenProvider.setHeaderAccessToken(response, newAccessToken.getValue());

                redisRepository.save(newRefreshToken);

                return MemberDTO.builder()
                        .accessToken(newAccessToken.getValue())
                        .member(member.get())
                        .build();
            }
        }
    } catch(ExpiredJwtException e) {
        // Refresh Token 만료 Exception
        throw new CustomException(ErrorCode.JWT_REFRESH_TOKEN_EXPIRED);
    }
    return null;
}
  • 토큰을 디코딩하여 Payload를 가져오는 JwtUtil의 getPayloadByToken을 구현하였습니다.
  • 아래와 같은 Jwt에 내장된 parser 기능이 있지만 굳이 Decode 코드를 추가한 이유는 이미 만료된 토큰이라 ExpiredJwtException이 발생하기 때문에 따로 Decode 하는 코드를 추가하였습니다. (만료되었지만 Payload 값은 가져오고 싶기 때문에...❗)
  • Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
     
  • Redis에 저장된 Refresh Token 만료까지 체크한 후  Access Token을 새로 만들어주고 보안을 위하여 새로운 Refresh Token을 발급해 새로 Redis에 저장함으로써 기존의 Refresh Token은 폐기합니다.
  • 위의 코드는 Filter가 아닌 Service단에서 이루어지는 코드라 Exception을 Custom 처리가 가능합니다. 따라서 Refresh Token이 만료된 경우 'JWT_REFRESH_TOKEN_EXPIRED' Custom 401 에러를 내려줍니다.

 

 

public static HashMap<String, String> getPayloadByToken(String token) {
    try {
        String[] splitJwt = token.split("\\.");

        Base64.Decoder decoder = Base64.getDecoder();
        String payload = new String(decoder.decode(splitJwt[1] .getBytes()));

        return new ObjectMapper().readValue(payload, HashMap.class);
    } catch (JsonProcessingException e) {
        log.error(e.getMessage());
        return null;
    }
}
  • 제가 구현한 Token을 Decode 하는 코드입니다😀😀

 

 

마치며


블로그에 글을 정리하면서 보니 구현하면서도 보안적으로 좀 다듬어야 할 부분들이 있다고 생각했는데 정말 크게 와닿네요😂😂

Github에 나중에 JWT 레포지터리를 따로 만들 생각인데 그때 보완해야 될 부분을 보완해서 올려야겠습니다ㅎㅎㅠ

 

 

 

GitHub - yujin-Kim-98/side-project-blog: Spring boot + React + MongoDB

Spring boot + React + MongoDB. Contribute to yujin-Kim-98/side-project-blog development by creating an account on GitHub.

github.com

 

'Backend > Spring' 카테고리의 다른 글

Spring boot에서 권한 Scope 처리를 해보자! (AccessDecisionManager, AccessDecisionVoter)  (0) 2022.10.30
Spring boot에서 JWT 구현(2)  (3) 2022.02.20
Spring boot에서 JWT 구현(1)  (0) 2022.02.19
Spring boot에서 첨부파일 업로드 (Apache Tika 파일 변조 체크)  (0) 2021.11.14
Spring boot에 lucy-xss-servlet-filter 적용하기  (0) 2021.07.11
'Backend/Spring' 카테고리의 다른 글
  • Spring boot에서 권한 Scope 처리를 해보자! (AccessDecisionManager, AccessDecisionVoter)
  • Spring boot에서 JWT 구현(2)
  • Spring boot에서 JWT 구현(1)
  • Spring boot에서 첨부파일 업로드 (Apache Tika 파일 변조 체크)
나는 유찌
나는 유찌
쩌리쨩
  • 나는 유찌
    유찌 개발 일기
    나는 유찌
  • 전체
    오늘
    어제
    • 분류 전체보기
      • 사이드 프로젝트
        • 게시판
        • 블로그(Spring boot + React.js ..
      • 데이터베이스
        • SQLD
      • 이슈 해결
      • Front
        • Javascript
        • Vue.js
        • HTML+CSS
      • Backend
        • Spring
        • ORM
        • JAVA
      • 공부
        • HTTP
        • OOP
        • 이것저것
        • 코딩테스트 | 알고리즘
      • Computer Science
        • Computer architecture
        • 데이터베이스
        • 운영체제
      • 일상
        • 독서
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    pessimisticlock
    LeetCode
    spring
    Kotlin AccessDecisionManager
    추리소설
    독서
    phantom read
    spring 격리수준
    Spring Boot
    Spring boot에서 JWT 구현
    access token
    mysql 격리수준
    AntPathMatcher
    mssql
    히가시노 게이고
    refresh token
    한국소설
    Spring Security AccessDecisionManager
    role scope
    DIRTY READ
    redis 분산락
    jwt
    권한 scope 처리
    Kotlin AntPathMatcher
    AccessDecisionVoter
    Access token 재발급
    jwt 로그인 구현
    JWT이란?
    Access Token Refresh Token
    웹 개발
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
나는 유찌
Spring boot에서 JWT 구현(3)
상단으로

티스토리툴바