Redis로 refreshToken 관리하기

동동주·2025년 2월 16일
1
post-thumbnail

Redis로 RefreshToken를 관리하려는 이유

기존에는 서버의 DB(MySQL)에 RefreshToken을 보관해서 사용하는 방식을 많이 사용했었다. 하지만 MySQL의 부하를 줄이기 위해 보다 효율적인 저장 방식이 필요하다고 생각했다. 해당 프로젝트를 같이 하던 팀원들과 논의하던 중 Redis를 활용해보자는 의견이 나왔고, 이에 Redis의 장점을 조사하게 되었다.

✔ Redis의 장점

✅ 메모리 기반 DB라서 초고속 성능 제공
✅ MySQL과 조합 & Key-Value 저장 방식으로 조회 성능 최적화 가능
✅ TTL(Time To Live) 기능 지원 → JWT 기반 세션 관리 최적화

Redis는 위와 같은 장점을 가지고 있었고 RefreshToken 특성상, 반복적이고 빠른 읽기 요구 & RefreshToken의 만료시간 관리가 필요했다. 결과적으로 NoSQL + In-Memory + expiretime 을 종합적으로 가지는 Redis가 토큰을 관리하는 데에 적합하다고 판단하여 해당 프로젝트에 적용하게 되었다.

기존의 프로젝트에서는 AccessToken만을 발급해 사용하고 있었는데, 여기에 RefreshToken을 발급하는 로직을 추가하고 이를 Redis에서 관리하게 하였다.

Redis 설정

의존성 추가

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

RedisConfig 추가

@Configuration
@EnableRedisRepositories
public class RedisConfig {

    @Value("${spring.data.redis.host}")
    private String host;

    @Value("${spring.data.redis.port}")
    private int port;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(host, port);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());

        // 일반적인 key:value의 경우 시리얼라이저
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());

        // Hash를 사용할 경우 시리얼라이저
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new StringRedisSerializer());

        // 모든 경우
        redisTemplate.setDefaultSerializer(new StringRedisSerializer());

        return redisTemplate;
    }
}
  • 나는 redis에 password를 따로 설정하지 않았는데, 이후에 찾아보니 해킹 문제가 있는 것 같아서 가능하면 password도 추가하는 걸 추천한다.

    password 설정을 추천하는 이유

  • return new LettuceConnectionFactory(host, port); :
    LettuceConnectionFactory를 사용하여 Lettuce 기반으로 Redis 연결을 하고 있다. Spring Boot에서는 기본적으로 Lettuce를 사용하며, 별도로 설정하지 않으면 Lettuce가 기본으로 선택된다.
  • RedisTemplate<String, Object>
    Redis의 key-value 방식을 이용해 데이터 저장 및 조회를 쉽게 처리할 수 있다는 걸 확인할 수 있다.
  • Redis 데이터를 저장하는 방식은 3가지가 있다.
    • 1️⃣ 일반적인 key:value의 경우 시리얼라이저
      • setKeySerializer(): Key를 문자열 형태로 변환하여 저장
        setValueSerializer(): Value를 문자열 형태로 변환하여 저장
    • 2️⃣ Hash를 사용할 경우 시리얼라이저
    • 3️⃣ 모든 경우
      • setDefaultSerializer()를 사용하면 모든 데이터(key, value, hash key, hash value 등)에 동일한 직렬화 방식 적용 가능

나의 경우, 가장 기본적인 1️⃣번 방식을 적용했다.

application.yml 추가

spring:
  data:
    redis:
      host: localhost
      port: 6379
  • redis의 host, port 설정

refreshToken 생성 로직 추가

기존에는 accessToken만 발급했던 로그인 로직에 refreshToken도 발급하고 저장할 수 있도록 수정했다.
1️⃣ JwtProvider

  public String createRefreshToken(User user) {
        if (user == null) {
            throw new IllegalArgumentException("유저가 존재하지 않습니다.");
        }

        Key key = getSecretKey();

        String refreshToken = Jwts.builder()
                .signWith(key)
                .header()
                .add("typ", "JWT")
                .add("alg", key.getAlgorithm())
                .and()
                .claim("id", user.getId())
                .expiration(Date.from(Instant.now().plusMillis(refreshExpiration)))
                .compact();

        redisUtil.set(user.getEmail(), refreshToken);
        redisUtil.expire(user.getEmail(), refreshExpiration, TimeUnit.MILLISECONDS);

        // Redis에 저장된 값 확인을 위한 로그 출력
        String storedToken = (String) redisUtil.get(user.getEmail());
        if (storedToken != null) {
            System.out.println("Redis에 저장된 refreshToken: " + storedToken);
        } else {
            System.out.println("Redis에 refreshToken이 저장되지 않았습니다.");
        }

        return refreshToken;
    }
  • Jwts.builder()를 통해 refreshToken을 생성했다.
  • redisUtil.set(user.getEmail(), refreshToken);를 통해 유저의 이메일과 refreshToken를 key-value 방식으로 저장했다.
    • ex) 아래와 같이 이메일을 통해 저장된 refreshToken을 확인할 수 있다.
    127.0.0.1:6379> GET user@example.com
    "abcd1234efgh5678"
  • redisUtil.expire(user.getEmail(), refreshExpiration, TimeUnit.MILLISECONDS); 를 통해 해당 유저의 이메일에 TTL(만료시간)을 설정하게 된다.
  • 그 밑에 작성된 주석은 중간에 저장이 됐는지 확인이 잘 안 됐던 경우가 있어서, Redis에 저장된 값을 확인하기 위해 로그를 출력해본 것이다! 확인하고 싶다면, 해당 코드를 추가해서 로그를 확인해보는 것도 도움이 될 것 같다.

RedisUtil 추가


@Component
@RequiredArgsConstructor
public class RedisUtil {
    private final RedisTemplate<String, Object> redisTemplate;

    public void set(String key, Object value) {
        redisTemplate.opsForValue().set(key, value);
    }

    public Object get(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    public boolean exists(String key) {
        return Boolean.TRUE.equals(redisTemplate.hasKey(key));
    }

    public void expire(String key, long timeout, TimeUnit unit) {
        redisTemplate.expire(key, timeout, unit);
    }

    public boolean delete(String key) {
        return Boolean.TRUE.equals(redisTemplate.delete(key));
    }

    public String extractEmailFromToken(String token) {
        try {
            return JWT.decode(token).getClaim("email").asString();
        } catch (JWTDecodeException e) {
            throw new IllegalArgumentException("유효하지 않은 JWT 토큰입니다.", e);
        }
    }
}
  • 해당 코드의 메서드를 이용하여 JWT 기반 인증 시스템에서 Redis를 통해 효율적으로 토큰을 관리할 수 있다!

redis에 잘 저장되었는지 확인하기


위와 같이 127.0.0.1:6379> keys *를 통해 redis에 저장된 모든 키를 조회할 수 있다. 확인하고 싶은 사용자의 이메일이 user@example.com이라면 127.0.0.1:6379> GET user@example.com를 통해 해당하는 refreshToken를 확인할 수 있다.


참고: https://velog.io/@cutepassions/Redis%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-JWT-%ED%86%A0%ED%81%B0-%EA%B4%80%EB%A6%AC-feat.%EC%BF%A0%ED%82%A4
github 코드: https://github.com/cholog-project/beacon/pull/68/files

0개의 댓글