본문 바로가기
스프링

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

by 근즈리얼 2023. 1. 31.
728x90

오늘은 redis를 이용해서 jwt 로그아웃 기능을 포스팅 해보겠습니다.

 

우선, 간단하게 redis를 왜 이용해야 하는지 고민해보겠습니다.

 

왜 redis를 사용할까?

 

바로 로그아웃을 요청한 access token이 만료될 때까지 access token으로 오는 요청을 막기 위해서입니다.

 

아직 좀 부족하죠??

그래서 왜 redis인데?

 

그렇다면 access token이 만료가 될 때까지 어딘가에 저장이 되어있어야 하지 않을까요?

음... access token이 어딘가에 저장되고 그 어딘가에 저장이 되어 있으면 api 요청을 못하게 하면 되지 않을까요?

음... 그러면 어딘가에 저장을 하는데... 일정 시간(만료시간)이 지나고 저절로 삭제가 될 수는 없을까?

 

위의 모든 의문을 해결해주는 것이 레디스 였습니다.

레디스는 간단하게 어플리케이션 이외의 메모리라고 생각하면 되는데요!

어플리케이션이 꺼져도 상관없는 메모리 공간에 로그아웃을 요청한 Access token값을 저장하고 만료시간을 설정해 둔다면 끝! 입니다.

 

이제 말은 그만하고! 코드로 한번 보시죠!

 

우선 설정 부분입니다.

build.gradle

    // redis 의존성
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'

저는 gradle을 사용하기 때문에 build.gradle파일 안에 위의 의존성을 추가해줬습니다.

 

application.yml

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

application.yml 파일안에 위의 redis 설정을 넣었습니다.

 

이제 redisConfig 파일을 만드려고 하는데요! 이 파일안에는 application.yml안에 port와 host를 알아야 합니다.

어떻게 알 수 있을까요?

저는 @ConfigurationProperties를 이용해서 해결했습니다.

먼저, RedisProperties 클래스를 소개하고 RedisConfig를 소개하겠습니다.

 

RedisProperties
@Component
@ConfigurationProperties(prefix = "spring.redis")
@Getter
@Setter
public class RedisProperties {

    private int port;
    private String host;

    public int getPort() {
        return this.port;
    }

    public String getHost() {
        return this.host;
    }
}

- @ConfigurationProperties를 이용해서 spring.redis의 하위의 값들을 필드값으로 가져왔습니다.

 

RedisConfig

@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;
    }
}

- RedisProperties를 가져와서 host와 port를 알아내고 Lettuce를 이용해서 redis를 사용할 수 있게끔 만들어줍니다.

 

이제는 위의 연결된 레디스를 좀 더 사용하기 쉽게 만들어주는 클래스를 만들어 보겠습니다.

RedisUtil

@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));
    }
}

- 사실, get, set, delete, hasKey만 있으면 되는데 이따가 로그아웃을 위해서 blackList관련 메소드들을 만들어 뒀습니다.

 


이제 설정은 끝났으니 미리 만들어 뒀던 로그인 로직에 로그아웃 로직을 추가해 보겠습니다.

가장 먼저! 이 Access Token이 redis에 저장되어 있는지 즉! 로그아웃을 요청한 Access Token인지 체크하는 로직을 보겠습니다.

 

TokenProvider.class

// TokenProvider 부분
public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            // 추가된 부분
            if (redisUtil.hasKeyBlackList(token)){
                // TODO 에러 발생시키는 부분 수정
                throw new RuntimeException("로그아웃 했지롱~~");
            }
                return true;
        } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
            log.info("잘못된 JWT 서명입니다.");
        } catch (ExpiredJwtException e) {
            log.info("만료된 JWT 토큰입니다.");
        } catch (UnsupportedJwtException e) {
            log.info("지원되지 않는 JWT 토큰입니다.");
        } catch (IllegalArgumentException e) {
            log.info("JWT 토큰이 잘못되었습니다.");
        }
        return false;
    }

- 토큰의 유효성을 체크하는 메소드에서 토큰 값을 redis에 조회해보고 있으면 예외처리를 시키도록 만들었습니다.

- 예외처리 부분은 아직 못만들어서... 대충 만들어봤습니다..

 

이번에는 로그아웃 api와 실제 service로직을 보겠습니다.

AuthController.class

@DeleteMapping("/logout")
public ResponseEntity<String> logout(
        @AuthenticationPrincipal CustomDetails customDetails,
        @RequestBody TokenDTO tokenDTO
) {

    return ResponseEntity.ok(authService.logout(tokenDTO.getAccessToken(), customDetails.getUsers()));
}

AuthService.class

public String logout(String accessToken, Users users) {

    // refreshToken 테이블의 refreshToken 삭제
    refreshTokenRepository.deleteRefreshTokenByKey(users.getEmail());

    // 레디스에 accessToken 사용못하도록 등록
    redisUtil.setBlackList(accessToken, "accessToken", 5);

    return "로그아웃 완료";
}

- 클라이언트로부터 accessToken을 넘겨받습니다.

- 서버에서는 accessToken은 redis에 기록하고 Users의 정보를 받아서 fk가 user의 email로 저장된 refreshToken을 지워줍니다.

 


이제 코드부분이 끝났으니 간단하게 포스트맨을 이용해서 테스트해보겠습니다.

간단하게 

회원가입 -> 로그인 -> 특정 api 호출(성공?) -> 로그아웃 (레디스 키 등록?) -> 특정 api 호출(실패?)

위의 플로우로 테스트 해보겠습니다.

저는 도커로 redis를 이용하기 때문에 cmd창에

docker run -it --link myredis:redis --rm redis redis-cli -h redis -p 6379 명려어를 이용해서 redis에 접근해보겠습니다.

그리고 keys *이라는 명령어를 통해 아무것도 없음을 확인했습니다.

 

로그인

- 로그인은 성공했고 평범한 api를 호출해보겠습니다.

 

평범한 API

- test용 API를 호출했을 때 제가 원하는 response가 나오는 것을 확인할 수 있었습니다.

 

이번에는 로그아웃 요청을 해보겠습니다.

로그아웃

- 로그아웃이 완료되었음을 확인했습니다.

 

이제 !! 

로그아웃을 했으니 레디스에 key가 잘 등록되어 있는지!! 그리고 평범한 API를 호출했을 때 '로그아웃 했지롤~~'이 잘 나오는지 체크해보겠습니다.

 

평범한 API 호출

 

RunTimeException이 발생한 모습

 

redis에 key가 넣어져있고 내가 설정한 시간대로 ttl이 나오는 모습

 

위의 프로세스에 따라서 제가 원하는 대로 로그아웃이 진행되었음을 알 수 있습니다!!!

로그아웃 성공!!!

 

하지만 아쉬운 부분도 있는데요!!

- TTL 시간을 일단은 정해준 것 -> access token의 만료시간을 계산해서 시간을 넣어줄 수 있도록 개선할 예정입니다.

- RunTimeException으로 처리한 부분 -> 당연히 다음 단계는 예외처리에 대해서 만들 생각이지만... 포스트맨의 response로 내려갔으면 더 멋있었을 텐데... 

 

참고한 블로그

https://wnwngus.tistory.com/65

 

SpringBoot 프로젝트에 JWT 토큰 인증 방식 구현하기

Refresh Token + Access Token + BlackList 전략으로 로그인 인증 구현하기 저는 이전에 진행 중이던 프로젝트에서 JWT 토큰 인증 방식을 선택해 프로젝트를 진행하였습니다. 프로젝트를 진행하면서 팀원들

wnwngus.tistory.com

 

728x90

댓글