오늘은 redis에 대해 알아보고 redis를 적용하여 redis를 통한 refreshToken을 관리해보자. 먼저 캐시에 대해 알아보자.
캐시는 한번 읽은(처리된) 데이터를 임시로 저장하고, 필요에 따라 전송, 갱신, 삭제하는 기술이다. 보통 서버의 메모리를 사용하는 경우가 많은데 매번 Disk로부터 데이터를 조회하는 것보다 훨씬 빠른 I/O 성능을 얻을 수 있다. 하지만 서버가 다운되거나 재부팅되는 경우 데이터가 사라지는 휘발성의 성격을 가지고 있다. 따라서 영속적으로 보관할 수 없는 임시적으로 보관하고 빠르게 해당 정보에 접근하기 위한 용도로 사용되어야 한다.
Redis의 경우, 주기적으로 Disk에 백업해 둘 수 있다.
redis는 key-value 저장소로 Sql 없이 List, Hash, Set, Sorted Set 등 여러 형식의 자료구조를 지원하는 NoSQL이다. 메모리에 상주하며 DBMS의 캐시 솔루션으로 주로 사용된다.
리프레시 토큰이란 액세스 토큰의 유효기간을 짧게 하여 보안도 높이고, 편의성도 챙기기 위해 존재하는데, Refresh Token은 Access Token이 만료되었을 때, 새로 발급해주는 토큰이라고 보면 된다.
1. 로그인을 하면 Access Token 과 Refresh Token을 발급해준다. ( Access 토큰만 클라이언트 쿠키에 저장 & Access 토큰과 Refresh 토큰을 레디스에 저장)
2. 클라이언트는 API를 호출할 때마다 발급받은 Access Token을 활용하여 요청을 한다.
3. 토큰을 사용하던 중, 만료되어 Invalid Token Error가 발생한다면 사용자가 보낸 Access Token으로 레디스의 Refresh Token을 찾아보고 Refresh 토큰이 유효하다면, Access Token을 다시 발급해준다.
4. Redis에 Refresh Token과 짝을 이루는 Access Token을 새로 발급한 토큰으로 업데이트한다.
5. 만약, Refresh Token도 만료되었다면, 다시 로그인을 하도록 요청한다.
6. 만약 사용자가 로그아웃을 하면, refresh token을 삭제하고 사용이 불가하도록 한다.
refresh token 개변을 활용하려면, 불가피하게 서버측에서 토큰 정보를 저장할 공간이 필요한데, 만료가 되는 영구적 데이터가 아니고 Mysql과 같이 데이터베이스에 직접 저장하기에는 중요한 데이터가 아니라고 보고 레디스를 사용하기로 하자.
레디스는 key-value 쌍으로 데이터를 관리할 수 있는 데이터 스토리지이다. 모든 데이터를 메모리에(메인 메모리인 RAM) 저장하고 조회하는 in-memory 데이터 베이스 이다.
인메모리 상태에서 데이터를 처리하기 때문에, 다른 DB들보다 빠르고 가볍다는 장점이 있다.
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-redis', version: '3.2.0'
implementation 'org.springframework.session:spring-session-data-redis:3.1.1'
redis:
host: localhost
port: 6379
@Configuration
public class RedisConfig {
@Value("${redis.host}")
private String redisHost;
@Value("${redis.port}")
private int redisPort;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisHost, redisPort);
}
@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;
}
}
스프링 애플리케이션에서 redis를 사용하기 위해 Config 파일을 작성하여 빈을 등록해준다.
Lettuce로 RedisConnectionFactory를 통해 Redis와 연결하고 redisTemplate()매서드로 RedisTemplate 객체를 생성하고 구성한다. 이때 Redis 연결 팩토리를 설정하고, 키와 값을 직렬화(serializer)한다.
Spring 에서 Redis를 사용하는 방법에는 2가지가 있다.
Spring 에서 Redis를 사용하는 방법에는 2가지가 있다.
첫번째 방법은 crudrepository방식이다.
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder
@RedisHash(value = "refresh_token")
public class RefreshToken {
@Id
private String authId;
@Indexed
private String token;
private String role;
@TimeToLive
private long ttl;
public RefreshToken update(String token, long ttl) {
this.token = token;
this.ttl = ttl;
return this;
}
}
위와 같이 엔티티를 작성하여
public interface RefreshTokenRepository extends CrudRepository<RefreshToken, String> {
Optional<RefreshToken> findByToken(String token);
Optional<RefreshToken> findByAuthId(String authId);
}
와 같이 작성하여 마치 JpaRepository를 사용하듯 사용하는 방법이다.
필자는 두번째 방법인 템플릿을 사용하여 구현해 보겠다.
다음과 같이 RedisUtil을 작성한다.
@Component
@RequiredArgsConstructor
public class RedisUtil {
private final RedisTemplate<String, Object> redisTemplate;
public void save(String key, Object val, Long time, TimeUnit timeUnit) {
redisTemplate.opsForValue().set(key, val, time, timeUnit);
}
public boolean hasKey(String key) {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
public boolean delete(String key) {
return Boolean.TRUE.equals(redisTemplate.delete(key));
}
}
이렇게 하면 다음과 같이 refreshToken을 생성할때 redis를 사용하여 refreshToken을 저장할 수 있다.
public String createJwtRefreshToken(CustomUserDetails customUserDetails) {
Instant issuedAt = Instant.now();
Instant expiration = issuedAt.plusMillis(refreshExpMs);
String refreshToken = Jwts.builder()
.header()
.add("alg", "HS256")
.add("typ", "JWT")
.and()
.claim("email", customUserDetails.getUsername())
.claim("is_staff", customUserDetails.getStaff())
.issuedAt(Date.from(issuedAt))
.expiration(Date.from(expiration))
.signWith(secretKey)
.compact();
redisUtil.save(
customUserDetails.getUsername(),
refreshToken,
refreshExpMs,
TimeUnit.MILLISECONDS
);
return refreshToken;
}
public boolean validateRefreshToken(String refreshToken) {
// refreshToken validate
String username = getUserEmail(refreshToken);
//redis 확인
if (!redisUtil.hasKey(username)) {
throw new SecurityCustomException(TokenErrorCode.INVALID_TOKEN);
}
return true;
}
와 같이 Redis를 사용하여 refresh token의 유효성을 검사하게 할 수있다. refresh token이 Redis에 저장되어 있는지 확인하여 유효성을 검증한다.