로그아웃 흐름
Redis를 사용해서 로그아웃을 요청한 AccessToken을 블랙리스트에 등록해둬야합니다. AccessToken의 유효기간이 남아있다면 탈취되었을 경우 악용할 수 있기 때문입니다. 그렇기 때문에 Redis에 토큰을 저장해둠으로써 해당 토큰으로 접근하는 것을 막을 수 있습니다.
- 클라이언트는 로그아웃을 요청한다.
- 토큰값이 유효하다면 로그아웃 처리를 하면서 해당 AccessToken을 Redis에 블랙리스트로 등록한다(Redis DB에 저장).
- 블랙리스트로 등록된 AccessToken은 서버에 접근할 수 없다.
- 블랙리스트로 등록된 토큰은 만료시간이 지나면 자동으로 삭제된다.
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);
}
}
'Spring > Server' 카테고리의 다른 글
Spring JPA + JWT로 로그인 구현하기(5) - 예외처리 (1) | 2023.10.20 |
---|---|
Spring JPA + JWT로 로그인 구현하기(4) - 인증 API 구현 (1) | 2023.10.04 |
Spring JPA + JWT로 로그인 구현하기(3) - Configuration 구현 (0) | 2023.08.18 |
Spring JPA + JWT로 로그인 구현하기(2) - 엔티티 구현 (0) | 2023.08.05 |
Spring JPA + JWT로 로그인 구현하기(1) - 기본 설정 (0) | 2023.08.04 |