본문 바로가기

SpringBoot

JWT, Redis를 활용하여 RefreshToken을 관리해보자!

JWT를 사용해서 로그인기능을 구현할 때, 보통은 RefreshToken도 함께 발급을 받는다.

그 이유는 간단히 말하면, AccessToken은 탈취 당할 위험이 있으니까!

 

좀 더 자세하게 말해보면

액세스 토큰은 일반적으로 클라이언트의 메모리나 로컬 스토리지, 쿠키에 저장된다.

이 저장소가 외부 공격자에 의해 침해될 경우 토큰이 쉽게 탈취될 수 있는거다.

 

외부 공격자는 아래 두가지 공격으로 해당 토큰을 탈취한다.

 

  • XSS(크로스 사이트 스크립팅): 악성 스크립트를 통해 브라우저의 로컬 스토리지에 접근하여 토큰을 탈취
  • CSRF(크로스 사이트 요청 위조): CSRF 공격을 통해 쿠키에 저장된 토큰을 이용해 인증된 요청을 위장

또는, 네트워크로 전송되는 과정에서 탈취가 될 수도 있는데

 

  • HTTPS가 아닌 HTTP를 통해 토큰이 전송되면 패킷 스니핑 공격으로 토큰이 노출
  • MITM(중간자 공격)으로 토큰 전송 데이터를 가로채는 시나리오도 있음

 

그렇기때문에 탈취 위험이 있는 accessToken은 짧게 두고 

refreshToken이라는걸로 해당 토큰이 안전한지에 대한 유효성 검사 및 accessToken이 만료되었을때, 이를 기준으로 재발급을 진행하는거다.

 

그럼 해당 토큰을 왜? Redis에 저장하는가? 

 

그 이유를 알기위해, 인메모리 db인 레디스의 특성을 알 필요가 있다.

 

  • Redis는 데이터를 디스크가 아닌 메모리(RAM)에 저장한다.
  • 메모리 접근 속도는 디스크에 비해 월등히 빠르며, 이는 대부분의 조회 요청이 마이크로초 단위로 처리되는 주요 이유이다.
  • Redis는 단일 스레드로 동작하며, 멀티스레드 기반 DB와 달리 컨텍스트 스위칭(스레드 간 작업 교환)으로 인한 오버헤드가 없다.
  • 이는 Redis가 높은 처리량을 유지하는 데 기여한다.
  • Redis는 단순한 키-값 저장소뿐만 아니라, 리스트, 해시, 셋, 정렬된 셋 등 다양한 데이터 구조를 지원한다.
public void saveToken(String key, String value, long duration) {
    redisTemplate.opsForValue().set(key, value, duration, TimeUnit.MILLISECONDS);
}
  • 이들 데이터 구조는 특정 조회 및 업데이트 작업에 최적화되어 있다.

 

일반 로컬에 저장하는것과 redis에 저장했을 때의 속도 비교

 

이처럼 Redis는 대부분의 연산에서 MySQL/PostgreSQL보다 약 10~100배 빠른걸 확인할 수 있다.

 

아래 코드들은 레디스에서 토큰을 관리하는 코드이다.

package com.swyp.playground.common.redis;

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class RedisService {

    private final StringRedisTemplate redisTemplate;

    public RedisService(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    // 토큰 저장
    public void saveToken(String key, String value, long duration) {
        redisTemplate.opsForValue().set(key, value, duration, TimeUnit.MILLISECONDS);
    }

    public void saveRefreshToken(String email, String refreshToken, long duration) {
        String refreshKey = "refresh_" + email;
        redisTemplate.opsForValue().set(refreshKey, refreshToken, duration, TimeUnit.MILLISECONDS);
    }
    // 토큰 조회
    public String getToken(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    // 토큰 삭제
    public void deleteToken(String key) {
        redisTemplate.delete(key);
    }
    public void deleteRefreshToken(String email) {
        String refreshKey = "refresh_" + email;
        redisTemplate.delete(refreshKey);
    }

    // 토큰 검증
    public boolean isTokenValid(String key) {
        return redisTemplate.hasKey(key);
    }
}

 

실제 유저가 로그인을 하게 되면

 

 

유저의 이메일과함께

액세스토큰이 출력되는걸 확인할 수 있다.