일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- 코딩테스트 준비
- 1주일회고
- 개발자부트캠프추천
- 구글 OAuth login
- 인프콘 2024
- 전략패턴 #StrategyPattern #디자인패턴
- 커스텀 헤더
- 디자인패턴
- 디자인 패턴
- infcon 2024
- @FeignClient
- jwt
- Spring multimodule
- 99클럽
- Python
- 취업리부트코스
- 항해99
- TiL
- 개발자 취업
- 파이썬
- JavaScript
- 프로그래머스
- DesignPattern
- 단기개발자코스
- 빈 충돌
- jwttoken
- spring batch 5.0
- 빈 조회 2개 이상
- 프로그래머스 이중우선순위큐
- KPT회고
- Today
- Total
m1ndy5's coding blog
[SpringBoot 3.0] JWT로 로그아웃을???(feat. Redis) 본문
[SpringBoot 3.0] JWT로 로그아웃을???(feat. Redis)
정민됴 2024. 1. 25. 00:35JWT가 무엇일까? 가 궁금한 사람은
https://m1ndy5.tistory.com/89
위 글을 먼저 읽고 오시길!!ㅎㅎ
JWT가 무엇인지 알았다면 JWT의 장점 중에 하나는 바로 Stateless하다는 것을 알았을 것이다!
즉, 서버가 이 토큰을 저장하고 있지 않다는 것인데... 여기서 궁금증이 생겼다.
JWT 토큰은 클라이언트 쪽에서 보관한다. 서버는 보관하지 않는다.
그럼 로그아웃을 할 때 클라이언트에 있는 토큰을 어떻게 없애지...??
JWT와 연관되서 로그아웃이 가능한거야?? 라는 궁금증을 품고 답을 찾아 해맸다!!!
그 결과 다음과 같은 로직을 찾을 수 있었다.
- 로그아웃을 요청한다.
- 해당 accesstoken을 blacklist에 저장한다.
- 요청의 accesstoken이 blacklist에 있는지 확인하고 있다면 토큰을 사용하지 못하게 막는다.
blacklist??? 그러면 얘는 어떻게 만들어야할까.. 새로운 테이블을 또 파야하나?? 하고 생각하고 있을 때 많은 사람들이 redis를 사용해서 구현한 것을 찾아볼 수 있었다.
Redis
Redis는 key-value 타입의 인메모리 저장소로서 데이터베이스, 캐시, 메세지 브로커 등으로 사용되는 고성능 NoSQL Database이다.
인메모리 저장소란?
컴퓨터의 메인 메모리 RAM에 데이터를 올려서 사용하는 방법을 말한다.
이 방법은 다른 저장공간에서 데이터를 가져오는 것보다 수백배 빠르므로 빠른 속도가 가장 큰 장점이다.
하지만 컴퓨터의 RAM 용량을 따라 가기 때문에 메인 데이터베이스로 사용하기엔 무리가 있다.
어?? 그런데 자바 ConcurrentHashMap을 쓰면 되지 않나 어짜피 로컬에 저장되는 건 똑같은데?? 라고 생각하겠지만
https://stackoverflow.com/questions/20376901/using-redis-to-cache-java-objects-why-it-should-be-better-than-a-concurrenthash
이 글을 읽어보면 ConcurrentHashMap은 외부 access가 필요하지 않고 토탈 크기가 많이 크지 않고 등등 Redis가 가진 능력(?)보다 더 가볍게 원할 때 사용하면 좋다는 것을 알 수 있다.
Redis 사용법
먼저 Redis를 사용하려면 해줘야하는 세팅들이 있다.
// redis 의존성
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
의존성을 추가해주고
redis:
pool:
min-idle: 0
max-idle: 8
max-active: 8
port: 6379
host: localhost
yml에도 redis 관련된 설정들을 몇가지 해준다.
RedisProperties
package hanghae99.ditto.auth.support;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "spring.redis")
@Getter
@Setter
public class RedisProperties {
private int port;
private String host;
}
yml파일에 적어놨던 값들을 넣어주고
RedisConfig
package hanghae99.ditto.global.config;
import hanghae99.ditto.auth.support.RedisProperties;
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.repository.configuration.EnableRedisRepositories;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@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;
}
}
redis연결을 도와줄 빈과 redis틀을 등록시켜준다.
RedisUtil
package hanghae99.ditto.auth.support;
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);
}
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));
}
}
Redis를 사용할 수 있는 메서드를 정의했다.
나는 여기서 setBlackList와 hasKeyBlackList를 사용했는데, 로그아웃을 했을 때 setBlackList에 등록하고
accesstoken을 validate할 때 hasKeyBlackList로 사용할 수 없는 토큰인지 확인했다.
AuthService
package hanghae99.ditto.auth.service;
import hanghae99.ditto.auth.domain.MemberAuthenticationCodeEntity;
import hanghae99.ditto.auth.domain.MemberAuthenticationCodeRepository;
import hanghae99.ditto.auth.dto.request.AuthenticateCodeRequest;
import hanghae99.ditto.auth.dto.request.LoginRequest;
import hanghae99.ditto.auth.dto.request.LogoutRequest;
import hanghae99.ditto.auth.dto.request.SendEmailAuthenticationRequest;
import hanghae99.ditto.auth.dto.response.EmailAuthenticationResponse;
import hanghae99.ditto.auth.dto.response.LoginResponse;
import hanghae99.ditto.auth.support.JwtTokenProvider;
import hanghae99.ditto.auth.support.RedisUtil;
import hanghae99.ditto.global.entity.UsageStatus;
import hanghae99.ditto.member.domain.Member;
import hanghae99.ditto.member.domain.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.Optional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class AuthService {
private final MemberAuthenticationCodeRepository memberAuthenticationCodeRepository;
private final MemberRepository memberRepository;
private final EmailService emailService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
private final JwtTokenProvider jwtTokenProvider;
private final RedisUtil redisUtil;
public HttpEntity<?> logout(LogoutRequest logoutRequest){
if(!jwtTokenProvider.validateToken(logoutRequest.getToken())){
return new ResponseEntity<>(
new LoginResponse(0L, "이미 로그아웃한 멤버입니다."), HttpStatus.BAD_REQUEST);
}
redisUtil.setBlackList(logoutRequest.getToken(), "accessToken", 5);
return new ResponseEntity<>(
new LoginResponse(1L, "로그아웃 완료"), HttpStatus.OK
);
}
}
jwtTokenProvider.validateToken에 토큰이 블랙리스트에 있는 토큰이면 false를 리턴하게 했다.
Redis라는 새로운 개념을 알게되서 재미있었던 구현이었던 것 같다!
이로써 JWT로 로그아웃 구현하기 끝~~!!
'Toy Projects > Ditto - Discuss Today's Topic' 카테고리의 다른 글
Ditto 프로젝트 멀티 모듈 도입 3편 - Gateway 도입하기 (2) | 2024.02.06 |
---|---|
Ditto 프로젝트 멀티 모듈 도입 2편 - Entity 연관관계 끊기 (0) | 2024.02.06 |
Ditto 프로젝트 멀티 모듈 도입 1편 - 모듈 나누기에 대한 고민 (1) | 2024.02.02 |
DDD? MSA? 멀티 모듈? (0) | 2024.01.31 |
[SpringBoot 3.0] 이메일 인증을 통한 회원가입 기능 만들기 (0) | 2024.01.24 |