Redis&RabbitMQ 통합 JWT 인증 시스템

최인호·2025년 3월 31일

JWT 인증 시스템?

사용자가 로그인할 때 발급되는 토큰
이 토큰은 사용자의 정보를 담고 있어서, 사용자가 API 호출할 때 매번 "나는 이 사람이 맞습니다!"라고 증명하는데 사용됨
이 JWT에는 짧은 시간 동안만 유효하고, 만료시 다시 로그인하거나 Refresh Token을 이용해 새로운 AccessToken이 필요

🚨 JWT의 문제점?

로그아웃한 사용자의 JWT를 어떻게 무효화할까?

-> 기본적으로 JWT는 자체적으로 만료되기 전까지 유효함
근데 사용자가 로그아웃했거나, 보안상의 이유로 강제 로그아웃을 해야한다면 어떻게 처리할까?

그냥 토큰 지워버리면 되는거 아닌가요?

JWT는 클라이언트 측에 저장되기 때문에 서버가 JWT를 직접 삭제하거나 만료시킬 방법은 존재하지 않음

💡 세션기반 인증방식과 비교해본다면?

  • 세션은 서버에서 관리되므로 사용자가 로그아웃하면 서버에서 세션을 삭제할 수 있음. 하지만 서버 메모리 혹은 DB에 작업해야하므로 서버마다 세션을 저장해야해서 확장성이 떨어짐

토큰을 분산 시스템에서 공유해야 할 때 어떻게 동기화 하지?

-> 여러 서비스에서 인증을 공유할 때, 한 서비스에서 로그아웃 시 다른 서비스에서도 즉시 반영되어야 함


1. Redis란?

1.1 Redis의 역할

토큰 블랙리스트와 Refresh Token 저장

  • Redis는 데이터를 메모리에 저장해서 엄청 빠르게 읽고 쓸 수 있다.
  • 로그아웃된 JWT를 기억하고 RefreshToken을 안전하게 저장하기 위해서 사용

블랙리스트 관리(로그아웃된 JWT저장)

  • 사용자가 로그아웃하면 JWT를 Redis에 넣고 "이 토큰은 더 이상 사용 불가!"로 저장
  • API 호출시마다 Redis를 확인해서, 블랙리스트에 있는 토큰이면 거부

Refresh Token 저장

  • Access Token은 짧은 시간이 지나면 만료되는데
  • 우리가 사용하는 SNS 인스타를 예시로 보면 1시간마다 로그아웃되면 사용자 경험이 현저히 낮아질 것
  • 이때 활용되는게 Refresh Token인데, 사용자가 다시 로그인 하지 않도록 이 토큰을 발급해서 새로운 Access Token을 발급해준다.
  • 단, 금융 서비스와 같이 보안에 민감한 부분에는 짧은 토큰 만료기간을 설정한 후 만료시, 다시 로그인 하도록 하며 SNS와 같이 사용자 경험이 중요한 서비스의 경우 리프레쉬 토큰 만료 기간을 길게 잡아서 편리한 서비스로 만듬

확장성

  • 서버가 여러개여도 Redis가 중앙에서 JWT 블랙리스트를 관리하면 문제 없음

휘발성

  • 다만 휘발성이 있기에, 서버가 꺼지면 데이터가 사라질 수 있음
  • 따라서 캐시로 많이 사용됨

1.2 Redis 대표적인 기능

  • Key-Value 저장소 : "user:123" → {"name": "Alice", "age": 25} 처럼 저장 가능
  • 캐싱 : 자주 쓰는 데이터를 저장해서 DB 부하를 줄임
  • 토큰 저장 : RefreshToken 저장, JWT 블랙리스트 관리

2. RabbitMQ란?

2-1. RabbitMQ의 역할

각각의 서비스가 독립적으로 동작하면서도, 서로 데이터를 주고 받을 수 있도록 도와주는 메시지 중계소

💡 쉽게 말하면?

  • auth 서비스가 blog 서비스로 회원가입 할거야! 라고 메세지를 보내고 싶을 때 RabbitMQ를 거쳐 전달
  • 두 서비스가 동시에 연결될 필요 없이 비동기적으로 통신 가능

MSA 구조에서 서비스들끼리 데이터 교환시 사용


🚀 결론적으로

  • JWT 인증시 Redis를 통해 빠른 데이터 조회(JWT 블랙리스트, Refresh Token 저장)
  • 분산 시스템에서도 중앙화된 Redis 메모리를 통해 인증 정보 공유 가능
  • 로그아웃 같은 이벤트를 여러 서비스에 동기화 하기 위해 Rabbit MQ
  • 서비스간 독립적인 메세지 전달을 지원하는 장점들을 활용해서

JWT 인증을 구현한다!


Step 1 : 기술 스택 선정

  • JWT: 인증 토큰 발급/검증

  • Redis: 토큰 블랙리스트 관리, Refresh Token 저장

  • RabbitMQ: 분산 시스템 간 이벤트 통신 (로그아웃 알림 등)

  • Spring Cloud Gateway: API Gateway 역할

  • Spring Security: 인증/인가 처리

  • Eureka: 서비스 디스커버리

Step 2 : 아키텍처

[Client]
  │
  ▼ HTTPS
[API Gateway] ───▶ [Auth Service] ↔ Redis (Token Storage)
  │                   │
  │                   ▼
  └──▶ [Blog Service] ←─ RabbitMQ (Logout Events)

Step 3 : Redis 통합 토큰 관리

  • Gateway에서 모든 클라이언트 요청을 필터링하며 JWT 검증을 수행
  • 블랙리스트에 등록된 토큰은 요청을 차단!
  • TokenRepository 사용해서 블랙리스트와 Refresh Token 관리
brew install redis
redis-server --version


위와 같이 설치하기

Redis 명령어

1. Foreground로 실행

redis-server

2. Background로 실행

brew services start redis
brew services stop redis
  • redis 테스트
redis-cli ping

Redis는 기본적으로 포트 6379에서 작동

Step 3-1 : Redis 설정

pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  • redis 의존성 추가

application.properties

spring.data.redis.host=localhost
spring.data.redis.port=6379
spring.data.redis.password=1234 
  • 레디스 정보 설정

Step 3-2 : RedisConfig.class

@Configuration
public class RedisConfig {

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(); // Lettuce는 비동기 방식으로 높은 성능 제공
    }

    @Bean
    public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer()); // Key를 문자열로 직렬화
        redisTemplate.setValueSerializer(new StringRedisSerializer()); // Value를 문자열로 직렬화
        return redisTemplate;
    }
}
  • RedisConnectionFactory는 레디스 서버와 연결을 관리하는 객체
  • Lettuce는 레디스와 통신할 때 비동기 방식으로 동작해 높은 성능을 제공
  • RedisTemplate는 레디스에 데이터를 넣고 가져오는 데 사용하는 주요 도구
    1. connectionFactory: 레디스 서버와 연결을 관리하는 객체를 연결
    2. KeySerializer: 키를 문자열로 변환(직렬화)
    3. ValueSerializer: 값을 문자열로 변환(직렬화)

💡 왜 직렬화가 필요할까?

레디스는 데이터를 네트워크를 통해 주고 받는데, 네트워크에서 데이터를 보내려면 객체를 문자열 혹은 바이트 형태로 바꿔야한다.
이 과정을 직렬화라고 한다.

  • RdisConfig는 실행 시 레디스 서버와 연결을 설정
  • RedisTemplate를 통해 레디스에 데이터를 저장하거나 가져올 수 있음
    - "user:1"라는 키에 "Inho"라는 값 저장하거나 가져오기

사용 예시

@Autowired
private RedisTemplate<String, String> redisTemplate;

public void useRedis() {
    // 데이터 저장
    redisTemplate.opsForValue().set("user:1", "John");

    // 데이터 가져오기
    String value = redisTemplate.opsForValue().get("user:1");
    System.out.println("user:1의 값은 " + value); // 출력: user:1의 값은 Inho
}
  • 데이터를 넣고 가져오는 간단한 예시

Step 3-3 : 토큰 블랙리스트 & 리프레쉬 토큰 관리 클래스

@Component
@RequiredArgsConstructor
public class TokenRepository {

    private final RedisTemplate<String, String> redisTemplate;

    // 블랙리스트에 토큰 추가 (만료 시간 설정)
    public void addToBlacklist(String token, long expiration) {
        redisTemplate.opsForValue().set(token, "blacklisted", expiration, TimeUnit.MILLISECONDS);
    }

    // 블랙리스트 여부 확인
    public boolean isBlacklisted(String token) {
        return Boolean.TRUE.equals(redisTemplate.hasKey(token));
    }

    // Refresh Token 저장 (유저 ID 기준)
    public void storeRefreshToken(String userId, String refreshToken) {
        redisTemplate.opsForValue().set(userId, refreshToken, 7, TimeUnit.DAYS);
    }

    // Refresh Token 조회 (유저 ID 기준)
    public String getRefreshToken(String userId) {
        return redisTemplate.opsForValue().get(userId);
    }
}

메소드 1 : 블랙리스트에 토큰 추가

 // 블랙리스트에 토큰 추가 (만료 시간 설정)
    public void addToBlacklist(String token, long expiration) {
        redisTemplate.opsForValue().set(token, "blacklisted", expiration, TimeUnit.MILLISECONDS);
    }
  • 특정 JWT 토큰을 블랙리스트에 추가
  • Redis에 토큰을 키로 저장 후 값은 blacklisted로 설정
  • 만료시간을 설정해서 밀리초 단위로 설정해서 자동으로 삭제

opsForValue() 메소드

1. 값 저장

redisTemplate.opsForValue().set("key", "value");

Redis에 "key"라는 이름으로 "value"를 저장
기본적으로 만료 시간이 설정되지 않으며, 값은 영구적으로 저장
2. 값 조회

String value = redisTemplate.opsForValue().get("key");

Redis에서 "key"에 해당하는 값을 가져옴
값이 없으면 null을 반환
3. 값 삭제

redisTemplate.delete("key");

특정 키에 저장된 데이터를 삭제
4. 값 저장 (만료 시간 설정)

redisTemplate.opsForValue().set("key", "value", 60, TimeUnit.SECONDS);

"key"에 "value"를 저장하고, 만료 시간을 60초로 설정
만료 시간이 지나면 Redis는 해당 데이터를 자동으로 삭제
5. 키 존재 여부 확인

boolean exists = redisTemplate.hasKey("key");

특정 키가 Redis에 존재하는지 확인
존재하면 true, 없으면 false를 반환

메소드 2 : 블랙리스트 여부 확인

 // 블랙리스트 여부 확인 
    public boolean isBlacklisted(String token){
        return Boolean.TRUE.equals(redisTemplate.hasKey(token));
    }
  • 특정 토큰이 블랙리스트에 등록되어 있는지 확인
  • 해당 토큰이 Redis에서 키로 존재하는지 검사

메소드 3 : 리프레쉬 토큰 저장

    // 리프레쉬 토큰 저장 
    public void storeRefreshToken(String userId, String refreshToken){
        redisTemplate.opsForValue().set(userId, refreshToken, 7, TimeUnit.DAYS);
    }
  • 특정 사용자ID를 기준으로 RefreshToken을 저장
  • 리프레쉬 토큰은 7일동안 유지되며, Redis TTL 기능으로 자동 삭제 됨

메소드 4 : 리프레쉬 토큰 조회

// 리프레쉬 토큰 조회 
    public String getRefreshToken(String userId){
        return redisTemplate.opsForValue().get(userId);
    }
  • 특정 사용자 ID를 기준으로 리프레쉬 토큰 조회

전체 동작 흐름

1. 로그아웃 처리(블랙리스트)

  • 서버가 로그아웃 요청을 받으면 해당 JWT를 addToBlacklist 메소드로 블랙리스트에 추가
  • 이후 인증 요청 시 isBlackliste 메소드로 JWT가 블랙리스트에 있는지 확인하고 요청 차단

2. 리프레쉬 토큰 관리

  • 사용자가 로그인시 storeRefreshToekn 메소드로 리프레쉬 토큰 저장
  • 클라이언트가 새로운 액세스 토큰 요청 시 리프레쉬 토큰 조회 후 검증

JWT 검증 필터 구현은 다음 게시글로 !

0개의 댓글