본 포스팅은 JWT와 access token, refresh token의 기본 개념을 숙지하고 있다는 가정하에 작성되었습니다.
이전 포스팅에서는 JWT 인증 서버의 로그인을 구현해보았습니다.
이번 포스팅에서는 Access Token이 만료되었을 시
Refresh Token을 통해 Access Token을 재발급 받는 reissue를 구현해보도록 하겠습니다.
@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
@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은 로그인을 할 시에 재발급 하는 것으로 구현하였습니다.
@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());
}
}
https://github.com/Development-team-1/just-pickup