개발바닥곰발바닥
728x90

Redis로 JWT RefreshToken 관리하기

JWT를 사용하여 유저 인증을 구현할 때, RefreshToken은 데이터베이스에 저장해두고 사용자가 토큰 재발급을 요청할 때 검사해야 한다.

그러나 RefreshToken에도 유효기간이 있기 때문에, RDBMS에 저장하면 배치를 이용하여 주기적으로 삭제를 해줘야 하는 번거로움이 생기는데 이럴 때 Redis를 사용하면 생성할 때 유효기간을 정해두고 따로 작업이 필요 없이 만료된 토큰은 삭제된다.

JWT 구현 부분은 생략하고 Redis 연동하는 부분만 설명하도록 하겠다. (redis 설치가 된 후의 진행과정)

1. bulid.gradle에 redis 의존성 추가

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

bulid.gradle에 redis 의존성을 추가하면 자동으로 lettuce라는 Redis Client가 같이 추가된다.

2. application.properties

spring.cache.type=redis
spring.redis.host =localhost
spring.redis.port=6379

properties 파일에 redis의 host와 port를 설정해준다. redis를 설치할 때 따로 포트를 변경하지 않았다면 기본 포트는 6379이다.

3. RedisConfiguration 작성

Spring Data Redis는 RedisTemplate를 이용한 방식과 RedisRepository를 이용한 두 가지 방식이 있는데, RedisTemplate 방식을 사용했다.

@RequiredArgsConstructor
@Configuration
@EnableRedisRepositories
public class RedisConfiguration{
    private final RedisProperties redisProperties;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort());
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        return redisTemplate;
    }

}

RedisConfiguration클래스에서는 RedisConnectionFactory 인터페이스를 implement 하는 LettuceConnectionFactory를 생성하여 반환해준다.

그리고 RedisTemplate를 Bean으로 등록해줘야 한다. 이 때, setKeySerializer, setValueSerializer는 Spring과 Redis 간의 데이터 직렬화, 역직렬화 시에 사용하는 방식을 설정하여 redis-cli로 데이터를 확인할 때 알아볼 수 있는 형태로 표시해준다.

Lettuce는 멀티 스레드를 지원하므로 RedisTemplate 하나만으로 여러 스레드에서 작동이 가능하다.

4. RefreshToken Redis에 저장

@Service
@RequiredArgsConstructor
public class AuthService {
private final RedisTemplate<String, Object> redisTemplate;

@Transactional(rollbackFor = Exception.class)
    public TokenDto login(LoginDto authRequest) {
        UsernamePasswordAuthenticationToken authenticationToken = authRequest.toAuthentication();

        Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);

        TokenDto tokenDto = tokenProvider.generateTokenDto(authentication);
				//redis에 RefreshToken 저장
        redisTemplate.opsForValue()
                .set("RefreshToken:" + authentication.getName(), tokenDto.getRefreshToken(),
                        tokenDto.getRefreshTokenExpiresIn() - new Date().getTime(), TimeUnit.MILLISECONDS); 

        return tokenDto;
    }
}

아까 Bean으로 등록한 redisTemplate을 가져와서 로그인 할 때 생성된 RefreshToken을 Redis에 저장해준다. Key는 RefreshToken:UserID, Value는 RefreshToken의 값이고 만료기한은 밀리세컨드로 설정해줬다.

토큰의 만료기간을 Redis에 같이 넣어줬기 때문에 만료기간이 되면 Redis에서도 해당 데이터가 삭제된다.

5. 재발급 요청시 Redis에서 토큰 가져오기

마지막 단계로 이제 JWT 토큰 재발급 요청이 들어왔을 때 Redis에서 Refresh Token을 가져와 검증한다.

@Transactional(rollbackFor = Exception.class)
    public ResponseEntity<?> reissue(TokenRequestDto tokenRequestDto) {
        if (!tokenProvider.validateToken(tokenRequestDto.getRefreshToken())) {
            return ResponseEntity.badRequest().body("Refresh Token이 유효하지 않습니다.");
            //throw new IllegalStateException("Refresh Token이 유효하지 않습니다.");
        }

        Authentication authentication = tokenProvider.getAuthentication(tokenRequestDto.getAccessToken());

        //Redis에서 Refresh Token 가져오기
        String refreshToken = (String) redisTemplate.opsForValue().get("RefreshToken:" + authentication.getName());
        if(!refreshToken.equals(tokenRequestDto.getRefreshToken())) {
            return ResponseEntity.badRequest().body("토큰의 유저 정보가 일치하지 않습니다.");
        }

        TokenDto tokenDto = tokenProvider.generateTokenDto(authentication);
				//새로 발급된 RefreshToken Redis에 저장
        redisTemplate.opsForValue()
                .set("RefreshToken:" + authentication.getName(), tokenDto.getRefreshToken(),
                        tokenDto.getRefreshTokenExpiresIn(), TimeUnit.MILLISECONDS);

        return ResponseEntity.ok(new Result<>(tokenDto));
    }

재발급 메소드에서 주석이 달린 부분들만 보면 Redis에 데이터를 넣고 빼는 과정을 이해하는데 무리가 없을 것 같다.

request로 들어온 RefreshToken과 redisTemplate로 가져온 RefreshToken의 값이 일치하는지 확인하고 일치한다면 tokenProvider에서 새 토큰을 발급하여 다시 Redis에 저장해주고 유저에게 새로 발급된 토큰을 반환해준다.

 

 

이번에 JWT를 구현하면서 Redis를 처음 사용해보게 됐는데, 실시간성이 크게 중요하지 않은 데이터의 경우 Redis로 캐싱을 구현하면 처리속도가 빨라진다고 하니 Redis로 캐시를 구현하는 방법도 공부해봐야 할 것 같다.

728x90
profile

개발바닥곰발바닥

@bestinu

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!