본문 바로가기
PlayGround/마실가실 리팩토링

[1년 후 마실가실] JWT와 로그아웃(1) Redis 설정

by HJ0216 2024. 8. 23.

1년 전 진행했던 마실가실 프로젝트를 🛠️리팩토링하며 정리한 내용입니다.

 

 

 

마실가실의 숨겨진 비밀..

한 번 로그인 한 고객님은.. 서버를 내리기 전까지 로그아웃할 수 없습니다.

 

이제는 보내드리려고 합니다.

안녕히 가세요, 고객님..

 

 

 

JWT, Token 방식으로 Session과 동작 과정을 비교해 볼 수 있습니다.

로그인

세션 방식

  * 서버는 세션 DB에 사용자의 정보를 저장 → 생성된 세션 ID를 (일반적으로) 쿠키에 넣어서 클라이언트에 반환
JWT 방식

  * 서버는 사용자의 정보 가운데 민감 정보가 아닌 정보로 토큰을 생성해 클라이언트에 반환

 

로그인 이후 요청

세션 방식

  * 클라이언트는 세션 ID를 포함한 쿠키를 서버에 전달 → 서버는 쿠키에서 세션 ID 추출 → 세션 DB에서 해당 아이디에 해당하는 레코드가 존재하는지 조회
JWT 방식

  * 서버는 서명 부분을 확인해 해당 토큰이 조작되었는지 판별 → 조작되지 않았다면 토큰으로부터 사용자의 아이디와 같이 식별할 수 있는 정보를 꺼내서 사용

 

JWT와 로그아웃

순수 JWT 방식을 사용할 때, 로그아웃은 클라이언트 측에서 쿠키나 로컬 스토리지에서 JWT 토큰을 삭제하거나 무효화하는 방식으로 구현할 수 있습니다. 그러나, 토큰이 유출될 경우 피해를 막을 수 있는 방법이 없다는 한계가 존재합니다.

그래서 토큰 유출에 대비해 Access Token의 유효 기간은 짧게, Refresh Token으로 Access Token을 재발급해주는 방법을 대안으로 사용합니다. 이 과정을 위해 Refresh Token은 서버에 저장되어야 하며, 이 때 데이터 베이스를 필요로 합니다. 그래서 Redis를 설정하고자 합니다.

 

또한 반대로, 로그아웃 시에는 Refresh Token으로 Access Token을 발급받을 수 없게 해야합니다. 따라서 Refresh Token용 데이터 베이스를 사용하되, 로그아웃한 사용자의 Refresh Token을 데이터베이스에 저장하여 해당 데이터 베이스에 정보가 있을 경우 Access Token 발급을 거절할 수 있도록 합니다. 이를 블랙리스트 방식이라 합니다.

 

왜 Redis인가

1. Redis는 메모리 기반의 NoSQL 데이터베이스로, 기존의 관계형 데이터베이스(MySQL 등)보다 훨씬 빠르게 데이터를 처리할 수 있음

  * 디스크 기반 데이터베이스이며, 많은 트래픽이 발생할 경우 성능에 문제가 생길 수 있음

2. 데이터에 TTL(Time-To-Live)을 설정할 수 있어, 만료된 JWT 토큰을 자동으로 삭제할 수 있음

  * 기존 RDB에 저장 시, 주기적으로 삭제해야하는 번거러움 발생

 

Springboot에 Redis 설정

1. build.gradle

implementation 'org.springframework.boot:spring-boot-starter

 

2. application.yml

spring:
  redis:
    pool:
      min-idle: 0
      max-idle: 8
      max-active: 8
    port: 6379
    host: 127.0.0.1

 

3. RedisProperties.java

@Component
@ConfigurationProperties(prefix = "spring.redis")
// application.yml 또는 application.properties 파일에서 spring.redis로 시작하는 설정 값을 이 클래스의 필드에 바인딩
@Getter @Setter
public class RedisProperties {
    private int port;
    private String host;
}

 

4. RedisConfig.java

@RequiredArgsConstructor
@Configuration
@EnableRedisRepositories
public class RedisConfig {

    private final RedisProperties redisProperties;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        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;
    }
}

 

5. RedisUtil.java

@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 Boolean.TRUE.equals(redisTemplate.delete(key));
    }

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

    public void setBlackList(String key, Object o, int minutes) {
        redisBlackListTemplate.setValueSerializer(new Jackson2JsonRedisSerializer(o.getClass()));
        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));
    }
}

 

 

+ 블랙 리스트 외 다른 로그아웃 방법

프론트에서 토큰 삭제

* storage에 있는 토큰을 삭제하는 방법

* 장점: 간단

* 단점: 토큰이 유출되었을 경우, 보안상 위험

 

 

++ 추가적인 보완책

IP 주소나 기기 정보를 토큰에 포함시켜 토큰을 사용하는 클라이언트 신원 확인
다른 기기나 IP 주소에서 사용되는 것을 방지

 

 

🙋‍♀️

본 포스트는 공부 목적으로 작성하였습니다.
보시는 도중 잘못된 부분이나 개선할 부분이 있다면 댓글로 알려주시면 수정하도록 하겠습니다.

 

📑

참고 자료

 

[스프링 시큐리티] redis를 이용한 jwt 로그아웃 만들기

오늘은 redis를 이용해서 jwt 로그아웃 기능을 포스팅 해보겠습니다. 우선, 간단하게 redis를 왜 이용해야 하는지 고민해보겠습니다. 왜 redis를 사용할까? 바로 로그아웃을 요청한 access token이 만료

geunzrial.tistory.com

 

 

[우테코] JWT 방식에서 로그아웃, Refresh Token 만들기(1): JWT의 Stateless한 특징을 최대한 살리려면?

안녕! 우아한테크코스 5기 [스탬프크러쉬]팀 깃짱이라고 합니다. 스탬프크러쉬 서비스의 소스 코드 바로가기 사장모드: stampcrush.site/admin 고객모드: stampcrush.site 💋 인트로 스탬프크러쉬는 사용

engineerinsight.tistory.com

 

 

대동덕지도 | Spring Boot에서 Spring Security + JWT로 로그아웃을 구현하자! (feat. Redis)

JWT 적용한 로그인은 구현 했는데 말입니다 ... 로그아웃은 어떻게 하지? JWT를 적용한 로그인 기능은 Access Token(AT), Refresh Token(RT)를 이용하여 구현해냈다. 참고: https://un-lazy-midnight.tistory.com/159 대동

un-lazy-midnight.tistory.com