[spring] refreshtoken 과 rtr, redis 기술
Redis
Redis is an open source (BSD licensed), in-memory data structure store used as a database, cache, message broker, and streaming engine.
Redis 란 DB, cache, Message broker, streaming engine 을 제공하는 인메모리 데이터 저장소 입니다.
인메모리
인메모리는 RAM 에 데이터를 올려서 사용하는 방법입니다. 메모리에 올리는 이유는 SSD, HDD 같은 저장공간에서 데이터를 읽는 것보다 속도가 빠르기 때문입니다.
하지만 속도가 빠른 대신 당연하게도 용량이 작다는 단점이 있습니다. 그리고 Redis 는 key, value 형태의 NOSQL 로 저장되며 다양한 데이터 구조를 지원하기는 하지만 복잡한 데이터를 저장하기는 어렵습니다.
또한 인메모리이기 때문에 서버에 충돌이 발생하거나 다운되면 저장된 값이 사라지게 됩니다.
따라서 Redis 는 주로 보조적인 수단으로 사용되는 캐시 DB 서버 입니다. (Persistence x)
특징
- Redis는 List, Set, Sorted Set, Hash 등과 같은 Collection을 지원합니다.
- List : redis Strings의 묶음 Collection
- Set : 중복되지 않는 값을 데이터로 갖는 Colelction
- Sorted Set : 가중치(Score)를 갖고 가중치(Score)에 따른 정렬된 순서로 중복되지 않는 값을 데이터로 가지는 Colelction
- Hash : Key 하나에 여러개의 Field와 Value로 구성된 즉, Key 내부에 또 다른 Key - value로 이루어진 Collection
- Dump 파일과 AOF(Append Of File) 방식으로 메모리 상의 데이터를 디스크에 저장할 수 있습니다.
- Master/Slave Replication 기능을 통해 데이터의 분산, 복제 기능을 제공하며 Query Off Loading 기능을 통해 Master는 Read/Write를 수행하고 Slave는 Read만 수행할 수 있습니다.
- 파티셔닝(Partitioning)을 통해 동적인 스케일 아웃(Scale Out)인 수평 확장이 가능합니다.
- Expiration 기능은 일정 시간이 지났을 때 메모리 상의 데이터를 자동 삭제할 수 있습니다.
- Redis는 Single Thread -> Atomic 보장
Redis 사용
원도우용 Redis 는 여기서 다운로드 받습니다. 윈도우용 Redis 다운로드
Redis 의 기본포트는 6379 입니다. 다운로드 이후 가동상태를 확인하려면 cmd 에서 다음과 같은 명령어를 입력해줍니다.
netstat -an | findstr 6379
잘 가동중임을 알 수 있습니다. 재부팅을 하면 자동으로 서비스로 등록이 되어 있고 실행이 되어서 따로 시작을 안해줘도 됩니다. 따로 시작을 하려면 Redis 폴더에서 redis-server.exe
를 실행하면 됩니다.
CLI 환경에서 저장
redis 폴더에서 redis-cli
로 redis 를 실행시킵니다.
- key, value 저장 :
set name hobeen
으로 저장합니다. - value 조회 :
get name
으로 값을 조회합니다. - key, value 삭제 :
del name
으로 값을 삭제합니다.
SpringBoot 연동
- 의존성을 추가합니다. (build.gradle)
implementation 'org.springframework.boot:spring-boot-starter-data-redis-reactive'
implementation 'org.springframework.boot:spring-boot-starter-web'
- application.yml 에 Redis 를 연결하기 위한 값을 넣습니다.
spring:
redis:
lettuce:
pool:
max-active: 10
max-idle: 10
min-idle: 2
port: 6379
host: 127.0.0.1
password:
해당 값들의 의미는 다음과 같습니다.
변수 | 기본값 | 설명 |
---|---|---|
spring.redis.database | 0 | 커넥션 팩토리에 사용되는 데이터베이스 인덱스 |
spring.redis.host | localhost | 레디스 서버 호스트 |
spring.redis.password | 레디스 서버 로그인 패스워드 | |
spring.redis.pool.max-active | 8 | pool에 할당될 수 있는 커넥션 최대수 (음수로 하면 무제한) |
spring.redis.pool.max-idle | 8 | pool의 “idle” 커넥션 최대수 (음수로 하면 무제한) |
spring.redis.pool.max-wait | -1 | pool이 바닥났을 때 예외발생 전에 커넥션 할당 차단의 최대 시간 (단위: 밀리세컨드, 음수는 무제한 차단) |
spring.redis.pool.min-idle | 0 | 풀에서 관리하는 idle 커넥션의 최소 수 대상 (양수일 때만 유효) |
spring.redis.port | 6379 | 레디스 서버 포트 |
spring.redis.sentinel.master | 레디스 서버 이름 | |
spring.redis.sentinel.nodes | 호스트:포트 쌍 목록 (콤마로 구분) | |
spring.redis.timeout | 0 | 커넥션 타임아웃 (단위: 밀리세컨드) |
- RedisConfig 를 설정합니다.
@Configuration
public class RedisConfig {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private String redisPort;
@Value("${spring.redis.password}")
private String redisPassword;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
redisStandaloneConfiguration.setHostName(redisHost);
redisStandaloneConfiguration.setPort(Integer.parseInt(redisPort));
redisStandaloneConfiguration.setPassword(redisPassword);
LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(redisStandaloneConfiguration);
return lettuceConnectionFactory;
}
@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 에 연결하기 위한
RedisConnectionFactory
와RedisTemplate
입니다. redisTemplate 을 이용하여 Redis 를 사용합니다.
- RedisController
package redis.hello.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
public class RedisController {
private final RedisTemplate<String, String> redisTemplate;
@GetMapping("/")
public String hello() {
return "Hello Redis";
}
@PostMapping("/redisTest")
public ResponseEntity<?> addRedisKey() {
ValueOperations<String, String> vop = redisTemplate.opsForValue();
vop.set("yellow", "banana");
vop.set("red", "apple");
vop.set("green", "watermelon");
return new ResponseEntity<>(HttpStatus.CREATED);
}
@GetMapping("/redisTest/{key}")
public ResponseEntity<?> getRedisKey(@PathVariable String key) {
ValueOperations<String, String> vop = redisTemplate.opsForValue();
String value = vop.get(key);
return new ResponseEntity<>(value, HttpStatus.OK);
}
}
redisTemplate
를 주입받습니다.redisTemplate.opsForValue();
으로 Redis 와 연결하여 key, value 를 넣고 꺼냅니다.
-
Postman 으로 확인해보겠습니다.
- POST
http://localhost:8080/redisTest
: 201 응답이 잘 옵니다. Redis 에 잘 저장되었습니다.
- GET
http://localhost:8080/redisTest/yellow
: key 값인yellow
를 넣었을 때 value 값인banana
가 잘 출력됩니다.
- POST
이를 활용해서 socceranalyst 프로젝트에 Redis 를 적용해보겠습니다.
RefreshToken 과 Redis
갑자기 Redis 를 언급한 이유는 RTR 적용을 위해서 refreshToken 을 Redis 에 저장하기 위함입니다. 다음에 언급할 주제이기도 한 RTR 은 Refresh Token Rotation 의 약자로, refreshToken 을 일회용으로 사용하고 폐기하는 정책입니다.
따라서 해당 refreshToken 을 유효한지 검증하고 처리하는 과정을 빠르게 하기 위해 Redis 를 사용하여 저장하고, 재발급 요청 시 DB 에 있는 refreshToken 인지 확인한 후에 재발급을 하게 됩니다.
RTR (Refresh Token Rotation)
JWT Token 은 보통 accessToken 과 refreshToken 으로 나눠 사용합니다. accessToken 은 인증 및 인가 권한을 가지며 만료시간이 짧습니다. refreshToken 은 만료기간이 길지만, accessToken 을 재발급하는 데만 사용됩니다.
RefreshToken 의 보안성 문제
하지만 이러한 Token 의 이원화에도 몇 가지 문제점이 있는데, 그 중 하나가 refreshToken 이 탈취당했을 때입니다. refreshToken 은 보통 2주의 만료기간을 가지니, 그 동안 attacker 는 계속해서 accessToken 을 발급받을 수 있습니다. Token 은 클라이언트의 상태를 보존하지 않는 stateless 상태이기 때문입니다.
RTR 동작 flow
RTR 정책이 동작하는 방식입니다.
- client 는 ID, PW 로 로그인을 요청합니다.
- 서버는
TokenProvider
로 accessToken, refreshToken 을 발급합니다. - refreshToken 은 사용자의 ID 등 식별할 수 있는 값으로 key 를 정하여 Redis 에 저장합니다.
- client 에게 accessToken, refreshToken 을 응답합니다.
- server 가 client 에서 만료된 accessToken 을 받으면,
- client 에게 401 에러를 반환합니다.
- client 는 accessToken 이 만료되었음을 확인하고 refreshToken 을 전송합니다.
- server 는 accessToken 에서 userID 를 key 로 하여 Redis 에서 refreshToken 을 조회합니다.
- Redis 는 refreshToken 을 반환하고, server 는 이 값을 대조합니다.
- 해당 값이 맞으면 accessToken, refreshToken 을 재발급 하고 refreshToken 은 다시 Redis 에 저장합니다.
- 새로운 accessToken, refreshToken 을 client 에게 반환합니다.
Attacker 에게 토큰을 탈취당할 경우
- 1번 accessToken, refreshToken 을 Attacker 에게 탈취당합니다.
- 1번 accessToken 만료 시 Attacker 는 재발급 요청을 합니다.
- 서버는 해당 토큰이 정상적이므로 Token 을 재발급하고 2번 refreshToken 은 Redis 에 저장합니다.
- 이후 재발급된 2번 accessToken, refreshToken 을 Attacker 에게 반환합니다.
- Client 측도 accessToken 만료 시 1번 refreshToken 으로 server 에게 재발급 요청을 합니다.
- 이 때 Redis 에 저장된 refreshToken 은 2번이므로 1번 refreshToken 은 유효하지 않습니다. 따라서 server 는 client 에게 재로그인 요구를 합니다.
- client 는 재로그인을 실시합니다.
- server 는 로그인 시 새로운 accessToken, refreshToken 를 발급하므로, 3번 accessToken, refreshToken 을 발급하고 3번 refreshToken 은 Redis 저장합니다.
- 이후 재발급된 3번 accessToken, refreshToken 을 client 에게 반환합니다.
- 2번 accessToken 이 만료되었을 때 Attacker 는 다시 2번 refreshToken 으로 accessToken 재발급을 요청합니다.
- server 는 Redis 조회 결과 2번 refreshToken 은 만료된 상태이므로 Attacker 에게 재로그인을 요구합니다.
- Attacker 는 client 의 ID, PW 를 알 수 없으므로 재로그인에 실패하게 됩니다.
문제점
- 이러한 방법은 JWT의 Stateless 방식에 어긋나는 행동입니다. Redis 에 refreshToken 을 저장함으로써 서버가 클라이언트의 상태를 보존하고 있기 때문입니다.
- Attacker 에게 accessToken, refreshToken 이 함께 탈취당했을 때 accessToken 이 만료되기 전까지 Attacker 는 사용자 정보에 접근할 수 있습니다. 이 점은 접속 IP 확인 등을 통해서 해결할 수도 있습니다.
보안에서 모든 상황을 만족할 수 없습니다. 적절한 Trade-off 가 필요합니다.
마치며
Redis, RTR 의 개념적인 부분만 개략적으로 알아봤습니다. 다음 포스팅은 [socceranalyst] 카테고리에서 직접 적용하는 걸 해보겠습니다.
- Redis 연동
- RefreshToken 저장 및 조회 구현
- Access Token 만료 시에만 새로운 Token을 발급 받을 수 있도록 구현. 그전에 발급받으려고 하면 Access, Refresh Token을 모두 폐기
- Attacker 가 Refresh Token을 사용하여 일찍 Access Token을 발급받으려는 상황 방지
+추가 : RTR 구현 코드 포스팅 링크입니다.
Ref.
Springboot + Redis 연동하는 초간단 샘플 만들어보기
댓글남기기