Redis 를 통한 JWT Blacklist 구현

Panda·2022년 4월 5일
7

Spring

목록 보기
19/42

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로 간단하게 설치하고 실행할수 있어서

실제 서버에도 배포하면서 설치하든 미리 설치를 하든 하면 될것같습니다.

이제 보안적으로도 안전하게 사용자 인증을 할수있게 되었네요!!!

참고 사이트

profile
실력있는 개발자가 되보자!

1개의 댓글

comment-user-thumbnail
2022년 9월 30일

글 잘 읽었습니다! 도움이 많이 됩니다. 그런데 혹시 AuthService 클래스에서 "Long expiration = tokenProvider.getExpiration(accessToken); " 이 부분에서 TokenProvider의 getExpiration 메소드가 나와있지 않아서요 ㅠㅠ 혹시 가능하시다면 TokenProvider 클래스 전체 코드 공유 가능하실까요?

답글 달기