[SpringBoot] RefreshToken 구현과 고찰

Mo-Greene·2024년 3월 21일

RefreshToken은 항상 구현해보자 싶었던 과정이었다.

회사에서는 항상 jwt토큰을 구현시 accessToken만을 구현 후 이후 개발과정으로 넘어갔고

그로인한 불안감을 가지고 있었다.

이번에 새로운 프로젝트에 투입하게 되면서 꼭 refreshToken을 구현해보자는걸 목표로 삼았고

그 과정을 기록해보려고 한다.


RefreshToken을 검색해본다면 여러가지 레퍼런스가 많고 Gpt에서도 쉽게 알려준다.
하지만 구현까지만! 있는 블로그들이 대다수이고 오히려 refreshToken으로 인증이 성공한 이후의 과정은 아예 없는 경우가 많아서 아쉬운 점을 느꼈다.

자 이제 내가 생각한 RefreshToken은 어떻게 구현되고 이후의 과정은 어떻게 되야하는지 알아보자.

가장 고민되는 부분은 2가지가 있다.

  1. 사실 accessToken의 만료시간은 짧게, refreshToken의 만료시간을 길게 설정하는데 만약 accessToken의 만료시간을 30분뒤로 잡아둔다면
    30분마다 refreshToken을 검증하는 request를 추가적으로 보내야한다.
    이게 만약 사용자가 늘어나면 늘어날 수록 리소스가 낭비되는 부분이 아닌가? 싶었고 그렇다고 이걸 획기적으로 해결할 만한 방법은 아직 찾지 못했다.

  2. 401에러 이후 새로운 accessToken을 받아오고 [*사진 과정 4-2]
    처음 요청을 재전송할 수 있을까?


현재 refreshToken을 구현하고 난 후 문제점은 만료된 accessToken으로 서버에 요청을 보내게 된다면

1. 첫번째 요청
2. 401 만료 예외처리를 받은 후 token 검증 요청
3. 새로운 accessToken을 받은 후 첫번째 요청을 재요청

이렇게 총 3번의 request를 보내게 된다는 것이다..
그리고 첫번째 요청이 만약 데이터의 개수가 몇만건의 단위가 된다면 세번째의 '첫번째 요청의 재요청'의 request가 또다시 날라가야 한다는 것이다. (겁나게 햇갈리네;)
물론 첫번째 요청의 경우 JwtFilter에 걸려 실제 DB의 I/O가 일어나진 않을테지만 과연 이게 맞을까? 싶은 찜찜함은 여전하다.


서론이 참 길었다.
위에 문제들은 차차 해결해나가야할 문제들이고
RefreshToken을 어떻게 구현했는지 소개한다.

build.gradle 세팅

dependencies {
    // redis
    implementation 'org.springframework.boot:spring-boot-starter-data-redis:3.2.0'
}

RefreshToken Entity

@Getter
@RedisHash(value = "refreshToken", timeToLive = 60 * 60 * 24 * 14) // 14 일
@Builder
@ToString
public class RefreshToken {

	@Id
	private String refreshToken;

	@Indexed
	private String accessToken;
}

간단하게 RefreshToken 테이블을 만들었다.
아직 Redis에 대해서 제대로 공부하지 않아서 정말 간단하게만 사용했다.


Controller

	@PostMapping("/refresh")
	public Response<?> getTokenRefresh(@RequestBody TokenRefreshRequest request) {

		return Response.ok(authService.getTokenRefresh(request));
	}

만료된 jwt로 인해 401에러를 프론트에 보내고
이후 프론트에서 전달한 refreshToken을 받는 Controller


TokenRefreshRequest

public record TokenRefreshRequest(
	String accessToken,
	String refreshToken
) {
}

accessToken과 refreshToken을 body값으로 가져온다. 만약 header에 집어넣는다면
jwtFilter에 걸려서 controller까지 들어오지 못하기 때문이다.


Service

	@Transactional
	public Token getTokenRefresh(TokenRefreshRequest request) {

		//refreshToken의 검증과정
		RefreshToken refresh = refreshTokenRepository.findById(request.refreshToken())
			.orElseThrow(NotFoundRefreshToken::new);

		if (!Objects.equals(refresh.getAccessToken(), request.accessToken())) {
			throw new NotFoundRefreshToken();
		}

		String email = jwtProvider.parseAccessToken(refresh.getAccessToken());

		return jwtProvider.changeAccessToken(email, refresh.getRefreshToken());
	}

jwtProvider 내부의 메서드의 경우는 혹시나 하는 일의 경우를 대비해 전체 코드를 가져오지 않았다.
하지만 jwtProvider는 다른 블로그나 여러 매체에서 구현한 예제 코드를 약간의 커스텀한 것이라
거의 비슷하다고 보면된다.

  1. 받아온 refreshToken으로 Redis 테이블을 조회한다.
    이 과정에서 만료된 토큰일 경우 'NotFoundRefreshToken' 예외처리가 된다.

  2. 그 후 혹시나 변조된 accessToken인지 확인하기 위한 방어코드를 집어넣었다.

  3. 무결한 accessToken을 확인 후 jwtProvider에서 다시 유효한 accessToken을 발급받고 그 Token을 반환한다.


구현 후

이렇게 RefreshToken의 구현을 완료하였다.

RefreshToken의 필요성과 구현에 대해서 적은 블로그는 참 많지만
그 이후의 과정이 없어서 조금 아쉬웠던 것 같다. (뭔가 현업과 동떨어진 느낌..?)

구현과정은 문제 없었지만 구현 후 서론에 적었던 문제점들은 과연 refreshToken을 위해 성능을 이렇게까지 포기해야 하는 것인가? 싶은 마음이 들었다.

혹시나 글을 읽고 좋은 방법을 알려준다면 참으로 큰 도움이 될 것 같다.

profile
아둥바둥 버텨라

0개의 댓글