jwt기반 사용자 인증을 구현하다가 logout을 구현해야하는데
단순히 Access Token제거 하고 Refresh Token 제거 할수 있기는 한데
문제는 Access Token의 유효기간이 여전히 살아있어서 누군가가 탈취했다고 가정하면 로그아웃을 하였더라도 그대로 사용할 수 있는 문제가 있습니다.
물론 만료기간이 30분으로 짧은편이긴 한데 혹시 모르니까 Access Token을 Blacklist로 저장하여 만료시키는 기능을 구현하려고 합니다!
공식 홈페이지 설명
이번 프로젝트 하면서 처음 알게된 아이인데
인메모리 데이터 저장소라서 캐싱, 세션관리 등등에 자주 쓰이는 것 같습니다.
이러한 특징을 이용하여 Blacklist를 구현하려고 합니다.
implementation("org.springframework.boot:spring-boot-starter-data-redis")
@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을 등록하게 됩니다.
@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에 존재하는지 확인만 하면 됩니다.
// 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 run -i -t --name redis -p 6379:6379 redis:alpine
이것만 치고 알아서 Redis image 다운하고 실행까지 됩니다
만약 백그라운드로 실행하고 싶으면 -d 옵션 붙여서 실행하면 됩니당!!
Reids 실행하고 돌려봤더니 정상적으로 logout 기능이 작동한 것을 확인했습니다!!
생각외로 코드는 간단하더라고요
다만 Redis 안깔고 실행시킨게 좀 코미디였는데 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
Docker로 간단하게 설치하고 실행할수 있어서
실제 서버에도 배포하면서 설치하든 미리 설치를 하든 하면 될것같습니다.
이제 보안적으로도 안전하게 사용자 인증을 할수있게 되었네요!!!
글 잘 읽었습니다! 도움이 많이 됩니다. 그런데 혹시 AuthService 클래스에서 "Long expiration = tokenProvider.getExpiration(accessToken); " 이 부분에서 TokenProvider의 getExpiration 메소드가 나와있지 않아서요 ㅠㅠ 혹시 가능하시다면 TokenProvider 클래스 전체 코드 공유 가능하실까요?