[MSA] JWT 인증 서버 구축하기 - 2.reissu

박상범·2022년 3월 6일
4

MSA

목록 보기
2/4

본 포스팅은 JWT와 access token, refresh token의 기본 개념을 숙지하고 있다는 가정하에 작성되었습니다.

이전 포스팅에서는 JWT 인증 서버의 로그인을 구현해보았습니다.

이번 포스팅에서는 Access Token이 만료되었을 시
Refresh Token을 통해 Access Token을 재발급 받는 reissue를 구현해보도록 하겠습니다.

구현 목표

  • access token이 만료되었을 시에 새로운 토큰이 발급되어야 합니다.
  • request로 전달 받은 access token과 refresh token에 대한 유효성 검사가 이루어 져야합니다.
  • access token과 refresh token이 유효하지 않다면 401 에러를 반환하여야합니다.
  • refresh token이 유효하다면 redis에 저장되어있는 (즉, 로그인 시 저장된) refresh token 값과 동일한지 확인합니다.
  • 새로 발급된 access token과 유효기간을 response body에 넣어 전달하여야 합니다.
  • refresh token의 경우 cookie의 httpOnly 속성을 true로 설정하여 넣어 전달하여야 합니다.

Controller

@RestController
@RequiredArgsConstructor
@RequestMapping("/auth")
@Slf4j
public class AuthController {

    private final RefreshTokenService refreshTokenService;
    private final CookieProvider cookieProvider;

    @GetMapping("/reissue")
    public ResponseEntity<Result> refreshToken(@RequestHeader("X-AUTH-TOKEN") String accessToken,
                                               @CookieValue("refresh-token") String refreshToken) {
        JwtTokenDto jwtTokenDto = refreshTokenService.refreshJwtToken(accessToken, refreshToken);

        ResponseCookie responseCookie = cookieProvider.createRefreshTokenCookie(refreshToken);

        // response header : 새로 발행된 refresh token
        // body : 새로 발행된 access token과 유효시간
        return ResponseEntity.status(HttpStatus.OK)
                .header(HttpHeaders.SET_COOKIE, responseCookie.toString())
                .body(Result.createSuccessResult(new RefreshTokenResponse(jwtTokenDto)));
    }

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    static class RefreshTokenResponse {
        private String accessToken;
        private String expiredTime;

        public RefreshTokenResponse(JwtTokenDto jwtTokenDto) {
            this.accessToken = jwtTokenDto.getAccessToken();
            this.expiredTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
                    .format(jwtTokenDto.getAccessTokenExpiredDate());
        }
    }
}

Service

  1. acces token에 닮겨 있는 userId를 가져옵니다.
  2. redis에 저장되어 있는 리프레쉬 토큰을 가져옵니다.
    • key: userId
    • value: refresh token
  3. refresh token을 검증합니다.
    • request에서 받은 refresh token의 유효성 검사를 진행합니다.
    • request에서 받은 토큰과 redisdp 저장되어 있는 토큰이 동일한지 확인합니다.
  4. access token의 userId가 유효한 아이디인지 확인합니다.
  5. 모든 검증이 끝났으므로 access token을 생성합니다.
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class RefreshTokenServiceImpl implements RefreshTokenService {
    private final UserDetailsService userDetailsService;
    private final JwtTokenProvider jwtTokenProvider;
    private final UserRepository userRepository;
    private final RefreshTokenRedisRepository refreshTokenRedisRepository;
    
    @Transactional
    @Override
    public JwtTokenDto refreshJwtToken(String accessToken, String refreshToken) {
        // 1. access token에 닮겨 있는 userId를 가져옵니다.
        String userId = jwtTokenProvider.getUserId(accessToken);
       
        // 2. redis에 저장되어 있는 (userId : refresh token) refresh 토큰을 가져옵니다
        RefreshToken findRefreshToken = refreshTokenRedisRepository.findById(userId)
                .orElseThrow(()
                        -> new RefreshTokenNotValidException("사용자 고유번호 : " + userId + "는 등록된 리프레쉬 토큰이 없습니다.")
                );

        // 3. refresh token 검증
        String findRefreshTokenId = findRefreshToken.getRefreshTokenId();
        if (!jwtTokenProvider.validateJwtToken(refreshToken) ||
                !jwtTokenProvider.equalRefreshTokenId(findRefreshTokenId, refreshToken)) {

            refreshTokenRedisRepository.delete(findRefreshToken);
            throw new RefreshTokenNotValidException("Not validate jwt token = " + refreshToken);
        }
        
        // 4. access token의 userId가 유효한지 검증합니다.
        User findUser = userRepository.findById(Long.valueOf(userId))
                .orElseThrow(() -> new NotExistUserException("유저 고유 번호 : " + userId + "는 없는 유저입니다."));

        // 5. access token 생성
        Authentication authentication = getAuthentication(findUser.getEmail());
        List<String> roles = authentication.getAuthorities()
                .stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());

        String newAccessToken = jwtTokenProvider.createJwtAccessToken(userId, "/reissu", roles);
        Date expiredTime = jwtTokenProvider.getExpiredTime(newAccessToken);

        return JwtTokenDto.builder()
                .accessToken(newAccessToken)
                .accessTokenExpiredDate(expiredTime)
                .refreshToken(refreshToken)
                .build();
    }
}

reissue에서는 access token만 발급하며 refresh token은 발급하지 않았습니다.
access token의 유효기간이 짧은 만큼 refresh token을 재발급하는 것은 비효율적으로 생각했습니다.
refresh token은 로그인을 할 시에 재발급 하는 것으로 구현하였습니다.

Exception Handler

@RestControllerAdvice
@RequiredArgsConstructor
@Slf4j
public class GlobalExceptionHandler {
    @ExceptionHandler(RefreshTokenNotValidException.class)
    public ResponseEntity customJwtExceptionHandler(RefreshTokenNotValidException e) {
        // 쿠키 삭제
        ResponseCookie responseCookie = cookieProvider.removeRefreshTokenCookie();

        return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                .header(HttpHeaders.SET_COOKIE, responseCookie.toString())
                .body(e.getResult());
    }
}
  • refresh token이 유효하지 않을 경우에는 401에러와 함께 쿠키를 제거하였습니다.

GitHub Code

https://github.com/Development-team-1/just-pickup

  • 모든 코드는 위 깃허브에서 확인하실 수 있습니다.
profile
배는 항구에 있을 때 가장 안전하다. 그러나 그것이 배의 존재의 이유는 아니다.

0개의 댓글