Programming/Spring Boot

JWT + Redis Logout 로그아웃 구현하기

Jan92 2021. 9. 25. 18:07

JWT, Redis logout

 

 

 

Spring Security + JWT 로그인 기능 파헤치기 - 1

로그인 기능은 거의 대부분의 애플리케이션에서 기본적으로 사용됩니다. 추가로 요즘은 웹이 아닌 모바일에서도 사용 가능하다는 장점과 Stateless 한 서버 구현을 위해 JWT를 사용하는 경우를 많

wildeveloperetrain.tistory.com

Spring Security + JWT + Redis + JPA 사용한 로그인 구현의 마지막 Logout 포스팅입니다.

 

Spring Security + JWT 기능 파헤치기 1, 2 포스팅과 Redis를 사용하여 RefreshToken을 저장하는 포스팅에 이어 4번째 포스팅입니다. (이전 내용은 위 링크를 참고 부탁드리겠으며, 전체 코드 GitHub 주소 및 참고 내용은 맨 하단에 링크해두겠습니다.)

 

* 잘못된 내용은 댓글로 알려주시면 공부하여 수정하겠습니다. 감사합니다.

 

 


 

 

JWT + Redis를 사용한 로그아웃 로직의 전체적인 흐름을 생각해봤습니다.

로그아웃 요청을 한다는 것은 로그인이 된 상태라는 것을 의미합니다. 그렇기 때문에 때문에 클라이언트로부터 액세스 토큰과 리프레시 토큰을 둘 다 받습니다.

이전 포스팅까지 Redis는 Refresh Token을 저장하는 용도로만 사용했지만 로그아웃 과정에서는 추가로 로그아웃 요청된 Access Token을 BlackList로 저장하는 기능이 추가됩니다.

 

* Redis 역시 일반 관계형 데이터베이스에서 처럼 table을 나눠서 사용할 수 있지만 지금은 Redis의 사용 용도가 저 두 가지밖에 없기 때문에 그대로 사용하였습니다.

 

AccessToken을 Redis에 BlackList로 등록할 때 필요한 부분은 요청 들어온 AccessToken의 남은 유효시간입니다.

로그아웃 성공된 회원의 AccessToken을 BlackList로 Redis에 등록할 때, 토큰 값 자체를 key로 두고, value로 logout이라는 값을 줬습니다. 이때 블랙리스트로 등록하는 액세스 토큰에 유효시간을 요청시 받은 엑세스토큰의 남은 유효시간만큼 줍니다. 이렇게 되면 로그아웃된 엑세스 토큰으로 요청이 들어왔을 때, 해당 토큰의 유효성이 남아있는 동안은 Redis에 해당 토큰 값이 key로 블랙리스트 등록되어 있을 것이기 때문에 로그인을 할 수 없습니다. 

그리고 요청하는 AccessToken의 유효시간이 만료되었을 때는 당연히 요청을 할 수 없고, 이때 블랙리스트에 등록된 해당 AccssToken 값도 자동으로 삭제됩니다.

 

이어서 코드로 내용을 살펴보겠습니다.

 

 


 

 

    public ResponseEntity<?> logout(UserRequestDto.Logout logout) {
        // 1. Access Token 검증
        if (!jwtTokenProvider.validateToken(logout.getAccessToken())) {
            return response.fail("잘못된 요청입니다.", HttpStatus.BAD_REQUEST);
        }

        // 2. Access Token 에서 User email 을 가져옵니다.
        Authentication authentication = jwtTokenProvider.getAuthentication(logout.getAccessToken());

        // 3. Redis 에서 해당 User email 로 저장된 Refresh Token 이 있는지 여부를 확인 후 있을 경우 삭제합니다.
        if (redisTemplate.opsForValue().get("RT:" + authentication.getName()) != null) {
            // Refresh Token 삭제
            redisTemplate.delete("RT:" + authentication.getName());
        }

        // 4. 해당 Access Token 유효시간 가지고 와서 BlackList 로 저장하기
        Long expiration = jwtTokenProvider.getExpiration(logout.getAccessToken());
        redisTemplate.opsForValue()
                .set(logout.getAccessToken(), "logout", expiration, TimeUnit.MILLISECONDS);

        return response.success("로그아웃 되었습니다.");
    }

Service 단 로그아웃 로직입니다.

  1. 먼저 요청받은 AccessToken 유효성을 검증합니다.
  2. 유효성 검증이 끝나고 액세스 토큰을 통해 Authentication 객체를 그리고 저장된 User email 정보를 가져옵니다.
  3. user email (Redis key 값)을 통해 저장된 RefreshToken이 있는지 여부를 확인하여 있다면 삭제합니다.
  4. 요청으로 들어온 액세스 토큰의 유효시간을 가져와서 해당 액세스 토큰을 키 값으로 하고, 유효시간을 적용시켜 Redis에 블랙리스트로 등록합니다.

Redis

(블랙리스트라고 하지만 실제로는 그냥 key : value -> 토큰 값 : logout이라는 로그아웃을 확인할 수 있게 등록하였습니다.)

 

 

 

    public Long getExpiration(String accessToken) {
        // accessToken 남은 유효시간
        Date expiration = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody().getExpiration();
        // 현재 시간
        Long now = new Date().getTime();
        return (expiration.getTime() - now);
    }

내부적으로 동작하는 메서드는 이전에 로그인 및 인증에 사용하던 JwtTokenProvider에 구현된 기능들을 사용하였고, 추가된 메서드는 토큰의 남은 유효기간(Expiration)을 가져오는 getExpiration() 메서드만 추가로 구현하였습니다.

 

* 전체 코드를 가져오기에는 코드가 너무 길기 때문에 JwtTokenProvider의 전체 코드 및 다른 부족한 부분은 아래 GitHub 코드를 참고 부탁드리겠습니다.

 

 


 

 

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        // 1. Request Header 에서 JWT 토큰 추출
        String token = resolveToken((HttpServletRequest) request);

        // 2. validateToken 으로 토큰 유효성 검사
        if (token != null && jwtTokenProvider.validateToken(token)) {
            // (추가) Redis 에 해당 accessToken logout 여부 확인
            String isLogout = (String)redisTemplate.opsForValue().get(token);
            if (ObjectUtils.isEmpty(isLogout)) {
                // 토큰이 유효할 경우 토큰에서 Authentication 객체를 가지고 와서 SecurityContext 에 저장
                Authentication authentication = jwtTokenProvider.getAuthentication(token);
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        chain.doFilter(request, response);
    }

다음으로는 실제 인증을 진행하는 jwtAuthenticationFilte.class에 추가된 내용입니다.

 

 

// (추가) Redis에 해당 accessToken logout 여부 확인
String isLogout = (String)redisTemplate.opsForValue().get(token);
if(ObjectUtils.isEmpty(isLogout)) {
	// 기존의 인증 인증 로직
}

기존 인증 과정에서 로그아웃으로 인해 추가된 부분은 redisTemplate으로 요청된 accessToken이 블랙리스트(Redis)에 등록되어있는지 확인하는 로직만 추가되었습니다. 

블랙리스트(로그아웃된 액세스 토큰)로 등록되지 않았다면 Authentication 객체를 가져와서 SecutiryContext에 저장하는 기존 인증 로직을 수행합니다.

 

 


 

 

    public ResponseEntity<?> reissue(UserRequestDto.Reissue reissue) {
        .
        .

        // 3. Redis 에서 User email 을 기반으로 저장된 Refresh Token 값을 가져옵니다.
        String refreshToken = (String)redisTemplate.opsForValue().get("RT:" + authentication.getName());
        // (추가) 로그아웃되어 Redis 에 RefreshToken 이 존재하지 않는 경우 처리
        if(ObjectUtils.isEmpty(refreshToken)) {
            return response.fail("잘못된 요청입니다.", HttpStatus.BAD_REQUEST);
        }
        if(!refreshToken.equals(reissue.getRefreshToken())) {
            return response.fail("Refresh Token 정보가 일치하지 않습니다.", HttpStatus.BAD_REQUEST);
        }

        .
        .
    }

한 군데 코드가 더 추가된 곳은 accssToken, refreshToken으로 토큰을 재발급 받는 과정입니다.

토큰을 재발급 받는 reissue 과정은 인증 여부가 security 설정에서 인증 정보가 없어도 허용하도록 해놨습니다. 그렇기 때문에 재발급 과정에서 추가 처리가 필요했는데요.

Redis에서 기존에 저장된 RefreshToken 정보를 가져올 때 로그아웃된 유저라면 RefreshToken이 삭제되어 Redis에 없을 것입니다. 이 부분을 이용하여 ObjectUtils.isEmpty() 메서드로 처리하였습니다.

 

* 마찬가지 내용이 길어져서 전체 코드는 첨부하지 않았습니다. 아래 GitHub 참고 부탁드리겠습니다.

 

 

로그아웃

 

코드 작성 후 Postman을 통해 로그아웃 및 로그아웃 된 토큰으로 요청하여 정상 작동 여부를 확인하였습니다.

추가로 발생하는 에러나 예외처리가 필요한 부분은 계속 추가하여 깃허브에 업로드 하겠습니다.

 

 

GitHub 전체 코드

 

GitHub - JianChoi-Kor/Login: Login (Spring Security, JWT, Redis, JPA )

Login (Spring Security, JWT, Redis, JPA ). Contribute to JianChoi-Kor/Login development by creating an account on GitHub.

github.com

 

 

참고하면 좋은 포스팅

 

Security + JWT + Redis 로그인 기능 구현 (최종)

이전 Spring Security + JWT 로그인 구현에서 마지막으로 Refresh Token 저장을 위한 Redis를 추가하고, 토큰 재발급 기능을 추가하여 로그인 기능을 완성하였습니다. Spring Security + JWT 로그인 기능 파헤치..

wildeveloperetrain.tistory.com

 

 

 

Spring Boot Redis 두 가지 사용 방법 RedisTemplate, RedisRepository

https://wildeveloperetrain.tistory.com/21 Redis란? 레디스의 기본적인 개념 (인메모리 데이터 구조 저장소) Redis란? Key, Value 구조의 비정형 데이터를 저장하고 관리하기 위한 오픈 소스 기반의 비관계형..

wildeveloperetrain.tistory.com