BlueNyang
[Spring Boot] Redis를 활용한 Refresh Token Rotation(RTR) 구현
Spring

[Spring Boot] Redis를 활용한 Refresh Token Rotation(RTR) 구현

BlueNyangBlueNyang
·
·
약 3분
·
# redis# refresh-token-rotation# ttl# spring-data-redis

지난 글에서는 JWT의 기본에 이어, 카카오 API를 활용한 OAuth2 로그인 구현 방법까지 알아보았습니다.

지금까지의 구현으로도 훌륭한 인증 시스템을 갖추었지만, JWT의 단점인 **'상태 비저장(Stateless)으로 인한 토큰 탈취 위험'**을 해결해야 완벽한 서비스 운영이 가능합니다. Access Token의 유효기간을 짧게 하고 Refresh Token을 도입하는 것이 일반적이지만, 만약 해커가 Refresh Token마저 탈취한다면 어떻게 될까요?

이번 글에서는 이 문제를 해결하기 위해 Redis를 활용하여 Refresh Token을 안전하게 관리하고, RTR(Refresh Token Rotation) 기법을 적용하는 방법을 알아보겠습니다.

1. 왜 Refresh Token 관리에 Redis를 사용할까?

Refresh Token을 DB에 저장하고 검증할 수도 있지만, 실무에서는 압도적으로 Redis를 선호합니다. 그 이유는 다음과 같습니다.

  • In-Memory 기반의 빠른 속도: 토큰 재발급은 빈번하게 일어나는 작업입니다. RDBMS(Disk 기반)에 매번 I/O를 발생시키는 것보다, 메모리 기반인 Redis를 사용하는 것이 서버 부하를 줄이고 응답 속도를 극대화할 수 있습니다.
  • 자체적인 TTL(Time To Live) 지원: Refresh Token은 만료 시간이 필수입니다. RDBMS는 만료된 토큰을 지우기 위해 별도의 스케줄러(Batch)를 돌려야 하지만, Redis는 데이터 저장 시 TTL을 설정하면 시간이 지남에 따라 알아서 데이터를 삭제해 줍니다.

2. RTR (Refresh Token Rotation) 이란?

RTR은 단어 뜻 그대로 Refresh Token을 계속 회전(교체)시키는 기법입니다.

기존에는 Access Token이 만료되면 동일한 Refresh Token으로 Access Token만 새로 발급받았습니다. 하지만 RTR을 적용하면, Access Token을 재발급받을 때마다 Refresh Token도 새로 발급하여 이전 Refresh Token은 즉시 폐기(1회용으로 사용)합니다.

이 방식을 사용하면 만약 해커가 Refresh Token을 탈취하더라도, 정상적인 사용자가 이미 해당 토큰을 사용해 버렸다면 해커의 토큰은 무효화됩니다. 반대로 해커가 먼저 사용해 버렸다면, 정상적인 사용자가 접근할 때 토큰이 만료/삭제된 상태이므로 시스템이 이를 감지하고 모든 기기에서 로그아웃 시키는 등의 강력한 보안 조치를 취할 수 있습니다.

3. 환경 세팅 (Redis & Spring Boot)

개발 환경이나 운영 중인 Linux 서버의 Docker 환경에서 손쉽게 Redis를 구성할 수 있습니다.

3.1. Redis Docker Compose 설정

yaml
services:
  redis:
    image: redis:alpine
    container_name: redis_token
    ports:
      - "6379:6379"
Important
본 글에서는 Docker Compose를 사용하여 아주 간단하게 Redis를 구성했지만, 운영 환경에서는 보안을 위해 비밀번호 설정 및 네트워크 격리 등의 추가 설정이 필요합니다. 또한, Redis는 라이센스가 변경되었으므로 운영 환경에 적용 시 라이센스 정책을 반드시 확인하시길 바랍니다.

3.2. Spring Boot 의존성 추가 (build.gradle)

gradle
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

3.3. application.yml 설정

yaml
spring:
  data:
    redis:
      host: localhost # 운영 서버 적용 시 해당 IP로 변경
      port: 6379

4. 본격적인 구현

Spring Data Redis가 제공하는 @RedisHash를 사용하면 JPA와 매우 유사한 형태로 직관적인 개발이 가능합니다.

Step 1. Redis 엔티티 (Domain) 생성

java
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.TimeToLive;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
// "refreshToken"이라는 prefix로 Redis에 저장됨
@RedisHash(value = "refreshToken")
public class RefreshToken {

    @Id
    private String refreshToken; // Token 자체를 ID(Key)로 사용

    private Long memberId;

    @TimeToLive
    private Long expiration; // TTL
}

Step 2. Redis Repository 생성

java
import org.springframework.data.repository.CrudRepository;
import java.util.Optional;

public interface RefreshTokenRepository extends CrudRepository<RefreshToken, String> {
    Optional<RefreshToken> findByRefreshToken(String refreshToken);
}

Step 3. 토큰 재발급 로직 구현 (RTR 적용)

이제 클라이언트가 토큰 갱신을 요청할 때 처리할 서비스 로직입니다. 핵심은 '조회 -> 검증 -> 삭제 -> 재발급 -> 저장' 의 사이클입니다.

java
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class AuthService {

    private final JwtProvider jwtProvider;
    private final RefreshTokenRepository refreshTokenRepository;

    @Transactional
    public TokenResponseDto reissueToken(String requestedRefreshToken) {

        // Redis에서 기존 Refresh Token 조회
        RefreshToken refreshTokenEntity = refreshTokenRepository.findByRefreshToken(requestedRefreshToken)
                .orElseThrow(() -> new IllegalArgumentException("유효하지 않거나 만료된 Refresh Token입니다. 다시 로그인해주세요."));

        // RTR: 사용된 기존 Refresh Token은 즉시 삭제
        refreshTokenRepository.delete(refreshTokenEntity);

        // 토큰 검증 로직 (만료 여부, 서명 등)
        jwtProvider.validateToken(requestedRefreshToken);

        Long memberId = refreshTokenEntity.getMemberId();

        // 새로운 Access Token 및 Refresh Token 발급
        String newAccessToken = jwtProvider.createAccessToken(memberId);
        String newRefreshToken = jwtProvider.createRefreshToken(memberId);

        // 새로운 Refresh Token을 Redis에 저장 (TTL 설정)
        RefreshToken newRefreshTokenEntity = new RefreshToken(
                newRefreshToken,
                memberId,
                604800L // 예: 7일
        );
        refreshTokenRepository.save(newRefreshTokenEntity);

        // 클라이언트에게 새 토큰 쌍 반환
        return new TokenResponseDto(newAccessToken, newRefreshToken);
    }
}

5. 마무리

이렇게 Spring Boot와 Redis를 활용하여 Refresh Token을 관리하고 RTR 기법까지 적용해 보았습니다. 이를 통해 토큰 기반 인증의 한계였던 탈취 위험을 크게 낮추고, 사용자에게는 끊김 없는 로그인 경험을 제공하면서도 보안은 챙길 수 있게 되었습니다.

BlueNyang
작성자BlueNyang
라이선스
CC BY NC
BlueNyang

BlueNyang

BlueNyang의 개발 log

카테고리

  • Development
  • Framework
  • Language
  • Dev Tools
  • DevOps & Infra
  • Studies

페이지

© 2026 BlueNyang. All rights reserved.

Made with Nuxt.js and Directus