로그아웃 흐름

Redis를 사용해서 로그아웃을 요청한 AccessToken을 블랙리스트에 등록해둬야합니다. AccessToken의 유효기간이 남아있다면 탈취되었을 경우 악용할 수 있기 때문입니다. 그렇기 때문에 Redis에 토큰을 저장해둠으로써 해당 토큰으로 접근하는 것을 막을 수 있습니다.

  1. 클라이언트는 로그아웃을 요청한다.
  2. 토큰값이 유효하다면 로그아웃 처리를 하면서 해당 AccessToken을 Redis에 블랙리스트로 등록한다(Redis DB에 저장).
  3. 블랙리스트로 등록된 AccessToken은 서버에 접근할 수 없다.
  4. 블랙리스트로 등록된 토큰은 만료시간이 지나면 자동으로 삭제된다.

RedisProperties

Redis에 연결하기 위한 기본 정보 Spring.redis에서 데이터를 가져온다.

package com.jh.jwt.configuration;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "spring.redis")
@Data
public class RedisProperties {

    private int port;
    private String host;
}

RedisConfig

package com.jh.jwt.configuration;


import com.jh.jwt.utils.KeyExpiredMessageListener;
import lombok.RequiredArgsConstructor;
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.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@RequiredArgsConstructor
@Configuration
@EnableRedisRepositories
public class RedisConfig {

    private final RedisProperties redisProperties;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        // RedisProperties를 통해 host와 port를 가져와 Redis에 연결한다.
        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;
    }

    // 만료된 토큰을 제거하기 위하여 MessageListenerAdapter, RedisMessageListenerContainer를 구성해서 메시지 리스너를 등록
    // "__keyevent@*__:expired"이러한 패턴 채널로부터 만료된 키에 대한 메시지를 수신하고 메시지가 도착하면 KeyExpiredMessageListener     // 를 호출하게 된다.
    @Bean
    public MessageListenerAdapter messageListenerAdapter() {
        return new MessageListenerAdapter(new KeyExpiredMessageListener());
    }

    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory,
                                                                       MessageListenerAdapter         
                                                                       messageListenerAdapter) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.addMessageListener(messageListenerAdapter, new PatternTopic("__keyevent@*__:expired"));
        return container;
    }
}

RedisUtil

Redis를 좀 더 쉽게 만들어주기 위해 Util을 생성

package com.jh.jwt.utils;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component
@RequiredArgsConstructor
public class RedisUtil {

    private final RedisTemplate<String, Object> redisTemplate;
    private final  RedisTemplate<String, Object> redisBlackListTemplate;

    public void set(String key, Object o, int minutes) {
        redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(o.getClass()));
        redisTemplate.opsForValue().set(key, o, minutes, TimeUnit.MINUTES);
    }

    // Redis의 저장된 값을 불러온다.
    public Object get(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    // Redis의 저장된 값을 삭제
    public boolean delete(String key) {
        return Boolean.TRUE.equals(redisTemplate.delete(key));
    }

    // 키값이 존재하는지 확인
    public boolean hasKey(String key) {
        return Boolean.TRUE.equals(redisTemplate.hasKey(key));
    }

    // 블랙리스트로 등록(Redis에 저장)
    public void setBlackList(String key, Object o, int minutes) {
        redisBlackListTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(o.getClass()));
        // Key: value 형태로 저장되며 minutes를 통해 key의 유효기간을 설정하여 자동으로 삭제될 시점을 정한다.
        redisBlackListTemplate.opsForValue().set(key, o, minutes, TimeUnit.MINUTES);
    }

    public Object getBlackList(String key) {
        return redisBlackListTemplate.opsForValue().get(key);
    }

    public boolean deleteBlackList(String key) {
        return Boolean.TRUE.equals(redisBlackListTemplate.delete(key));
    }

    public boolean hasKeyBlackList(String key) {
        return Boolean.TRUE.equals(redisBlackListTemplate.hasKey(key));
    }
}

KeyExpiredMessageListener

메시지가 도달하였을 때 실행되는 리스너이며 해당 클래스를 통해 삭제 기능이 수행된다.

package com.jh.jwt.utils;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.core.RedisTemplate;

@RequiredArgsConstructor
public class KeyExpiredMessageListener implements MessageListener {

    @Override
    public void onMessage(Message message, byte[] pattern) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();

        // 삭제될 key를 불러온다.
        String expiredKey = message.toString();

        // 만료된 키를 삭제한다.
        redisTemplate.delete(expiredKey);

    }
}

+ Recent posts