waveofmymind
기록하는 습관
waveofmymind
전체 방문자
오늘
어제
  • 분류 전체보기 (124)
    • 📝 정리 (5)
    • 🌊TIL (9)
    • 💻CS (1)
      • 자료구조 (1)
    • 📙Language (9)
      • ☕Java (6)
      • 🤖Kotlin (3)
    • 🍃Spring (28)
    • 👨🏻‍💻알고리즘 (67)
      • 프로그래머스 (59)
      • 백준 (3)
    • 👷DevOps (4)
      • 🐳Docker (2)
      • 🤵Jenkins (1)

블로그 메뉴

  • 홈
  • Spring
  • Java
  • 알고리즘

공지사항

인기 글

태그

  • Spring Security
  • resultset
  • 힙
  • 코틀린
  • 스프링
  • 챗GPT
  • kotlin
  • LeetCode
  • 스택
  • chat GPT
  • 완전탐색
  • JDBC
  • AOP
  • 스프링 시큐리티
  • til
  • 트랜잭션
  • sql
  • CORS
  • Connection
  • 다이나믹 프로그래밍
  • kotest
  • 통합테스트
  • 트랜잭션 전파
  • SpringAOP
  • mybatis
  • BFS
  • 스프링 부트
  • spring
  • spring boot
  • Open AI

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
waveofmymind

기록하는 습관

[Redis] Redis 캐시를 사용한 JWT 리프레시 토큰 관리하기
🍃Spring

[Redis] Redis 캐시를 사용한 JWT 리프레시 토큰 관리하기

2023. 4. 10. 00:51

이번 프로젝트부터 Spring Security를 사용하여 로그인을 구현하게 되었다.

그래서 그중 가장 많이 사용하는 JWT 토큰의 로그인 방식을 구현했다.

 

전체적인 흐름은 아래와 같이 구현했다.

  1. 클라이언트가 로그인을 요청
  2. 서버에서 유효성을 체크 후 응답으로 액세스 토큰과 리프레시 토큰을 반환한다. 유효 시간은 각각 2시간, 14일로 설정했다.
  3. 서버에서는 로그인 응답을 할 때, 아이디와 리프레시 토큰을 RefreshToken 엔티티를 생성하고 RDB에 저장한다.
  4. 시간이 지나 엑세스 토큰이 만료된다.
  5. 엑세스액세스 토큰이 만료되어도 리프레시 토큰의 유효기간이 길기 때문에 액세스 토큰 재발급 경로로 요청 시, 액세스 토큰 내에 페이로드로 담긴 아이디와 리프레시 토큰으로 유효성 검증을 한다.
  6. 이때 DB에 저장했던 RefreshToken의 아이디와 요청 아이디를 비교한다.
  7. 만약 아이디가 다르거나, 토큰이 다르거나, 토큰이 만료되었을 경우 예외를 발생시킨다.

그러나, 결국 리프레시 토큰도 만료가 되기 때문에  리프레시 토큰이 만료되면 RDB에 있던 만료된 사용자의 리프레시 토큰을 RDB에서 배치 작업을 통해 일일이 지워줘야 한다는 번거로움이 있었다. 

그래서 더 좋은 방법이 없을까 하다가 Redis라는 것을 알게 되었다.

 

RDB보다 Redis를 통해 리프레시 토큰을 관리할 때의 이점은 정리해보면 아래와 같다.

  1. 메모리에 저장되는 데이터에 접근하기 때문에 디스크 기반의 RDB를 사용할 때보다 조회 속도가 빠르다.
  2. 리프레시 토큰을 생성하고 저장할때 유효시간을 설정하므로 자동으로 만료되어야 하는 토큰에 알맞다.

그 외에도 Redis를 사용할 때의 장점은 많지만, 내가 실질적으로 체감되는 장점은 위 두 가지이다.

 

설정

우선, Spring-Data-Redis를 사용하기 위해 build.gradle에 의존성을 추가했다.

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

그 후 Redis 관련 Configuration을 생성했다.

 

RedisConfig.java

@Configuration
@Profile("dev")
public class RedisConfig {
    @Value("${spring.redis.host}")
    private String host;

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

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

 

나는 운영 서버에서 해당 프로젝트와 Redis를 각각 컨테이너로 실행시켜서 연결할 것이므로 dev 프로필에서 환경 변수를 초기화 시점에 불러 올 수 있도록 했다.

 

그리고 Redis 캐싱 설정을 위한 클래스를 다음과 같이 작성했다.

 

RedisCachingConfiguration.java

@RequiredArgsConstructor
@EnableCaching
@Configuration
public class RedisCachingConfiguration {
    private final RedisConnectionFactory redisConnectionFactory;

    @Bean
    public CacheManager redisCacheManager() {
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration
                .defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .entryTtl(Duration.ofHours(12));

        return RedisCacheManager
                .RedisCacheManagerBuilder
                .fromConnectionFactory(redisConnectionFactory)
                .cacheDefaults(redisCacheConfiguration)
                .build();
    }

    @Bean
    public DateTimeFormatter dateFormatter() {
        return DateTimeFormatter.ofPattern("yy.MM.dd");
    }
}

각각 캐싱을 구성하는데 필요한 정보로, 특이사항으로는

StringRedisSerializer를 사용해서 문자열 키를 직렬화 했으며, 객체 보관 기간을 12시간으로 설정했다.

 

RedisTemplate 사용하기

그리고 내가 Redis 기능을 쓸 곳은 리프레시 토큰을 생성할 때, 토큰이 아직 메모리상에 존재 유무를 확인할 때이다.

 

JwtTokenUtils.java

@Slf4j
@Component
public class JwtTokenUtils {

    private RedisTemplate<String, String> redisTemplate;


    private String SECRET;

    public JwtTokenUtils(@Value("${jwt.secret-key}") String SECRET,
                         RedisTemplate<String,String> redisTemplate) {
        this.SECRET = SECRET;
        this.redisTemplate = redisTemplate;
    }
    public static long ACCESS_EXPIRATION_TIME = 120 * 60 * 1000L; // 2시간
    public static long REFRESH_EXPIRATION_TIME = 14 * 60 * 60 * 24 * 1000L; // 14일

    public LoginToken createLoginToken(User user) {
        String refreshToken = refreshToken();
        redisTemplate.opsForValue().set(user.getAccount(), refreshToken, 14, TimeUnit.DAYS);
        return LoginToken.builder()
                .accessToken(AuthConstants.TOKEN_PREFIX.getValue() + accessToken(user))
                .refreshToken(AuthConstants.TOKEN_PREFIX.getValue() + refreshToken)
                .build();
    }

나는 단순이 key-value 쌍으로 메모리상에 저장할 것이기 때문에 StringRedisTemplate을 사용해도 되지만, 추후 확장성을 고려해 일단 RedisTemplate을 사용하기로 했고, key-value를 String-String으로 설정했다.

.opsForValue.set()

그리고 로그인 토큰을 리턴할때 .opsForValue().set(user.getAccount(),refreshToken,14,TimeUnit.DAYS); 로 메모리에 리프레시 토큰을 저장했다.

위 메서드는 다음과 같이 실행된다.

  1. opsForValue(): Redis에서 값을 다루는데 사용되는 ValueOperations 인터페이스의 인스턴스를 반환
  2. .set(...): ValueOperations 인터페이스에 정의된 set 메서드를 사용해서 Redis에 데이터를 저장한다. 4개의 파라미터는 각각
    • 첫 번째 인자: key를 설정할 수 있으며, 나는 account를 키로 사용하기 위해 .getAccount()를 사용했다.
    • 두 번째 인자: value를 설정할 수 있으며, 나는 생성한 리프레시 토큰으로 설정했다.
    • 세 번째 인자: 14는 TTL(Time-To-Live) 값을 설정하는 데 사용된다. 위에서 설정 정보에는 12시간으로 적었으나, 해당 메서드로 설정한 값이 더 우선순위로 적용된다.
    • 네 번째 인자: TimeUnit.DAYS는 위에서 14에 대한 단위 시간을 설정한다. 나는 1일로 정했으니 14 * 1일 해서 보관 기간이 14일이 된다.

즉 위 메서드를 정리하면, Redis 메모리에 key를 account, value를 리프레시 토큰으로 14일동안 보관하도록 설정한 것이다.

 

다음으로는 조회할때의 메서드를 확인해보자.

 

.opsForValue().get()

Optional<String> findRefreshToken = Optional.ofNullable(redisTemplate.opsForValue().get(account));
        findRefreshToken.ifPresentOrElse(findToken -> {
            if(!findToken.equals(refreshToken)) {
                throw new NotFoundException(ErrorCode.TOKEN_VERIFY_FAIL);
            }
        },
                () -> {
            throw new NotFoundException(ErrorCode.REFRESH_TOKEN_NOT_FOUND);
                });

get 메서드 인자로 위에서 정의했던 key 값을 줄 경우, 반환 값으로 value를 가져온다.

나는 토큰이 존재할 때 인자로 받았던 리프레시 토큰과 비교해서 다를 경우와 못찾았을 때 예외를 발생하기 위해 옵셔널로 감싸주었다.

 

정리

 

본 글에서는 리프레시 토큰을 관리하는 데에만 중점적으로 Redis를 다루어 보았다.

저장, 조회뿐이라 사용해보았다고 하기도 뭐하지만, 이런식이다~라는 개념을 잡을 수 있었다.

만약 다음에 Redis를 더 활용할 기회가 생기면 설정하는데 편하게 할 수 있을 것 같다.

 

 

 

 

 

 

'🍃Spring' 카테고리의 다른 글

[Spring] 테스트 클렌징시 deleteAll()을 사용할 때의 주의점  (0) 2023.05.13
[Spring] WebMvcTest에서 발생하는 SpringSecurity 의존성 문제 해결하기  (0) 2023.04.19
[Spring Security] CORS 문제 해결하기  (0) 2023.04.01
[Spring] 조회수 필드에 대해 동시성 문제 해결하기  (1) 2023.03.29
[Spring] Spring AOP를 이용한 권한 체크  (0) 2023.02.21
    '🍃Spring' 카테고리의 다른 글
    • [Spring] 테스트 클렌징시 deleteAll()을 사용할 때의 주의점
    • [Spring] WebMvcTest에서 발생하는 SpringSecurity 의존성 문제 해결하기
    • [Spring Security] CORS 문제 해결하기
    • [Spring] 조회수 필드에 대해 동시성 문제 해결하기
    waveofmymind
    waveofmymind

    티스토리툴바