Redis 를 통한 JWT Blacklist 구현
jwt기반 사용자 인증을 구현하다가 logout을 구현해야하는데
단순히 Access Token제거 하고 Refresh Token 제거 할수 있기는 한데
문제는 Access Token의 유효기간이 여전히 살아있어서 누군가가 탈취했다고 가정하면 로그아웃을 하였더라도 그대로 사용할 수 있는 문제가 있습니다.
물론 만료기간이 30분으로 짧은편이긴 한데 혹시 모르니까 Access Token을 Blacklist로 저장하여 만료시키는 기능을 구현하려고 합니다!
Redis란?
공식 홈페이지 설명
- Redis는 빠른 오픈 소스 인 메모리 키 값 데이터 구조 스토어입니다. Redis는 다양한 인 메모리 데이터 구조 집합을 제공하므로 다양한 사용자 정의 애플리케이션을 손쉽게 생성할 수 있습니다. 주요 Redis 사용 사례로는 캐싱, 세션 관리, pub/sub 및 순위표를 들 수 있습니다. Redis는 현재 가장 인기 있는 키 값 스토어로서, BSD 라이선스가 있고, 최적화된 C 코드로 작성되었으며, 다양한 개발 언어를 지원합니다.
이번 프로젝트 하면서 처음 알게된 아이인데
인메모리 데이터 저장소라서 캐싱, 세션관리 등등에 자주 쓰이는 것 같습니다.
이러한 특징을 이용하여 Blacklist를 구현하려고 합니다.
스프링에서 Redis 사용하기
의존성
implementation("org.springframework.boot:spring-boot-starter-data-redis")
Bean 등록
@Configuration
public class RedisConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory());
return redisTemplate;
}
}
여기서는 <String, Object> 형식의 Template를 생성하였는데 필요한 형식이 있으면 추가하여 Bean으로 등록하면 됩니다!
그리고 cache 기능, redisConnectionFactory 분리 등등 다양한 기능들을 추가할 수 있습니다.
로그아웃 기능
// AuthService
@Transactional
public void logout(String accessToken, String refreshToken) {
// 1. Access Token 검증
if (!tokenProvider.validateToken(accessToken)) {
throw new ApiException(BasicResponseMessage.UNAUTHORIZED);
}
// 2. Access Token 에서 authentication 을 가져옵니다.
Authentication authentication = tokenProvider.getAuthentication(accessToken);
// 3. DB에 저장된 Refresh Token 제거
Long userId = Long.parseLong(authentication.getName());
refreshTokenRepository.deleteById(userId);
// 4. Access Token blacklist에 등록하여 만료시키기
// 해당 엑세스 토큰의 남은 유효시간을 얻음
Long expiration = tokenProvider.getExpiration(accessToken);
redisUtil.setBlackList(accessToken, "access_token", expiration);
}
로그아웃을 실제 진행을 하게 되는데 DB에 저장된 RefreshToken을 삭제하고
Blacklist에 Access Token을 등록하게 됩니다.
Blacklist 기능
@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);
}
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
public boolean delete(String key) {
return redisTemplate.delete(key);
}
public boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}
public void setBlackList(String key, Object o, Long milliSeconds) {
redisBlackListTemplate.setValueSerializer(new Jackson2JsonRedisSerializer(o.getClass()));
redisBlackListTemplate.opsForValue().set(key, o, milliSeconds, TimeUnit.MILLISECONDS);
}
public Object getBlackList(String key) {
return redisBlackListTemplate.opsForValue().get(key);
}
public boolean deleteBlackList(String key) {
return redisBlackListTemplate.delete(key);
}
public boolean hasKeyBlackList(String key) {
return redisBlackListTemplate.hasKey(key);
}
}
Blacklist 등록은 되게 간단한데 그냥 RedisTemplate에다가
등록하려는 Access Token, object 값, 유효시간을 넣어주면 됩니다.
끄읕!!!! 이 아니라 이렇게 등록을 시켜뒀으니까
Access Token을 받을때마다 Blacklist에 존재하는지 확인만 하면 됩니다.
Blacklist 존재하는지 확인 (로그아웃 된 토큰인지)
// TokenProvider
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
if(redisUtil.hasKeyBlackList(token)) {
return false;
}
return true;
} catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
....
}
}
기존 Jwt 검증을 하는 부분에서 Blacklist에 추가된 Token인지 확인하고 검증을 하면됩니다
이제 진짜 끝!!!!
인줄 알았는데 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
이 상태로 돌리니까 에러가 뜨더라고요
redisconnectionfailureexception unable to connect to redis
이런 에러가 떴는데 저는 순간 ???? 뭐지 왠 connection 에러가 나지 라고 생각을 하였는데
생각해보니까 host랑 port지정한 것도 그렇고 설마 설마 Redis 프로그램이란걸 설치해서 돌려야되나???
라고 알아보니까 진짜로 설치해서 돌려야되더라고요 ㅋㅋㅋㅋㅋㅋㅋ
저 진짜 바보인것 같습니다.
Docker로 Redis를 설치 및 실행해보자
docker run -i -t --name redis -p 6379:6379 redis:alpine
이것만 치고 알아서 Redis image 다운하고 실행까지 됩니다
만약 백그라운드로 실행하고 싶으면 -d 옵션 붙여서 실행하면 됩니당!!
Reids 실행하고 돌려봤더니 정상적으로 logout 기능이 작동한 것을 확인했습니다!!
느낀 점
생각외로 코드는 간단하더라고요
다만 Redis 안깔고 실행시킨게 좀 코미디였는데 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
Docker로 간단하게 설치하고 실행할수 있어서
실제 서버에도 배포하면서 설치하든 미리 설치를 하든 하면 될것같습니다.
이제 보안적으로도 안전하게 사용자 인증을 할수있게 되었네요!!!
참고 사이트
- https://wnwngus.tistory.com/65?category=894390
- https://velog.io/@kenux/Redis-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8-Redis-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0
Author And Source
이 문제에 관하여(Redis 를 통한 JWT Blacklist 구현), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@boo105/Redis-를-통한-JWT-Blacklist-구현저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)