개인 프로젝트를 하면서 refreshToken에 대한 정보를 redis에 저장시켜 refreshToken 만료 시 만료되었다는 response를 내려주는 방법에 대해 정리했다.
이 글은 로컬에 redis가 깔려있는 상태에서 작성했습니다.
//redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
build.gradle
파일에 추가해주고 빌드 해준다.
spring:
redis:
host: localhost
port: 6379
application.yml
파일에 local의 redis 정보를 입력해준다.
package study.till.back.config.redis;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
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, String> redisTemplate() {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory());
return redisTemplate;
}
}
위 코드에 대한 설명을 하자면
클래스 선언 : @Configuration
어노테이션을 통해 이 클래스가 스프링 설정 클래스임을 명시한다.
속성 값 주입 : @Value
어노테이션을 통해 Redis 서버의 호스트 이름과 포트 번호를 프로퍼티 파일에서 가져와 속성에 주입한다.
Redis 연결 생성 : redisConnectionFactory()
메소드에서 LettuceConnectionFactory
인스턴스를 생성해 Redis 서버와의 연결 관리한다.
RedisTemplate 설정 : redisTemplate()
메소드에서 RedisTemplate
인스턴스를 생성하고, 키와 값의 직렬화 방식을 StringRedisSerializer
로 설정한다.
RedisTemplate에 연결 설정 : 앞서 생성한 redisConnectionFactory()
를 RedisTemplate
의 연결 팩토리로 설정한다.
이렇게 설정된 RedisTemplate
은 애플리케이션의 다른 부분에서 주입받아 Redis 데이터베이스와의 데이터 교환을 수행하는 데 사용된다.
@Slf4j
@Component
public class JwtTokenProvider {
private final Key key;
@Value("${jwt.accessExpirationTime}")
long accessExpirationTime;
@Value("${jwt.refreshExpirationTime}")
long refreshExpirationTime;
private final RedisTemplate<String, String> redisTemplate;
public TokenInfo generateToken(String memberPk, List<String> roles) {
long now = (new Date()).getTime();
Date accessTokenExpiresIn = new Date(now + accessExpirationTime);
// Access Token 생성
Claims claims = Jwts.claims().setSubject(String.valueOf(memberPk));
claims.put("roles", roles);
String accessToken = Jwts.builder()
.setClaims(claims)
.setExpiration(accessTokenExpiresIn)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
// Refresh Token 생성
String refreshToken = Jwts.builder()
.setClaims(claims)
.setExpiration(new Date(now + refreshExpirationTime))
.signWith(key, SignatureAlgorithm.HS256)
.compact();
redisTemplate.opsForValue().set(
refreshToken,
memberPk,
refreshExpirationTime,
TimeUnit.MILLISECONDS
);
return TokenInfo.builder()
.grantType("Bearer")
.accessToken(accessToken)
.refreshToken(refreshToken)
.build();
}
}
기존 토큰 생성 로직에
redisTemplate.opsForValue().set(
refreshToken,
memberPk,
refreshExpirationTime,
TimeUnit.MILLISECONDS
);
redisTemplate.opsForValue().set(key, value, timeout, unit)
메소드는 Redis의 String 데이터 타입을 다루는데 사용되는 명령어이다.
를 추가하여 Key, Value로 이루어진 redis에 Key
에는 refreshToken 정보
, Value
에는 사용자 ID값
을 저장하도록 했다.
토큰 발행은 로그인 시 해주기 때문에 테스트 해보았다.
로그인에 성공했으니 redis에 refreshToken
정보가 저장되어야 한다.
정상적으로 로그인 후 redis Key
에 refreshToken 정보
, Value에 사용자 ID
가 저장된걸 확인했다.
클라이언트쪽에서 헤더 정보에 token 정보를 담아 API 요청을 할때 accessToken이 만료되었을때 accessToken을 재발급해주는 API가 있다. 이제 그 코드에 redis에 refreshToken에 대한 값이 없다면 그에 맞는 response를 내려주는 코드를 짰다.
public ResponseEntity<TokenResponse> refreshToken(TokenRequest tokenRequest) {
String refreshToken = tokenRequest.getRefreshToken();
ValueOperations<String, String> stringValueOperations = redisTemplate.opsForValue();
String redisValue = stringValueOperations.get(refreshToken);
if (redisValue != null && jwtTokenProvider.validateToken(refreshToken)) {
Claims claims = jwtTokenProvider.parseClaims(refreshToken);
String newAccessToken = jwtTokenProvider.createAccessToken(claims);
TokenResponse tokenResponse = TokenResponse.builder()
.newAccessToken(newAccessToken)
.build();
return ResponseEntity.ok(tokenResponse);
}
else {
throw new ExpiredRefreshTokenException();
}
}
package study.till.back.service;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import study.till.back.config.jwt.JwtTokenProvider;
import study.till.back.dto.token.TokenInfo;
import study.till.back.dto.token.TokenRequest;
import study.till.back.dto.token.TokenResponse;
import study.till.back.exception.token.ExpiredRefreshTokenException;
@Service
@RequiredArgsConstructor
public class TokenService {
private final JwtTokenProvider jwtTokenProvider;
private final RedisTemplate<String, String> redisTemplate;
public ResponseEntity<TokenResponse> refreshToken(TokenRequest tokenRequest) {
String refreshToken = tokenRequest.getRefreshToken();
ValueOperations<String, String> stringValueOperations = redisTemplate.opsForValue();
String redisValue = stringValueOperations.get(refreshToken);
if (redisValue != null && jwtTokenProvider.validateToken(refreshToken)) {
Claims claims = jwtTokenProvider.parseClaims(refreshToken);
String newAccessToken = jwtTokenProvider.createAccessToken(claims);
TokenResponse tokenResponse = TokenResponse.builder()
.newAccessToken(newAccessToken)
.build();
return ResponseEntity.ok(tokenResponse);
}
else {
throw new ExpiredRefreshTokenException();
}
}
}
ValueOperations 인터페이스
는 Redis의 String 데이터 타입을 다루는 메소드들을 제공하고, 이를 통해 Redis의 String 데이터에 대한 CRUD(Create, Read, Update, Delete) 작업을 수행할 수 있다.
이 코드를 살펴보면
redisTemplate
의 opsForValue()
메소드를 호출하여 ValueOperations 인스턴스
를 가져오고 있고 redisValue
변수에 대한 null 체크를 하여 null이라면 예외처리를 해주었다.