티스토리 뷰

Redis로 로그아웃

토큰 방식에서 로그아웃 하기 위해서는 토큰의 만료시간을 만료시키거나 토큰 자체를 삭제해야 한다. 하지만 토큰의 만료시간은 직접 조작할 수가 없다. 실제로 로그인을 유지시키는 에세스 토큰을 서버가 가지고 있지 않기 때문이다. 토큰 자체를 삭제하는 방법도 불가능하다. 만약 요청에서 받아온 토큰을 삭제하더라도 클라이언트 입장에서는 그대로 들고있는 것이기 때문에 토큰을 삭제하는 의미가 없다. 그래서 다른 방법을 사용한다. 

 

 

사용자가 로그아웃 요청을 보낸다. 이 때 요청헤더에서 에세스 토큰을 찾아서 넘겨준다. 

@DeleteMapping("/logout")
    public void logout(HttpServletRequest request) {
        authService.logout(request.getHeader("ACCESS-TOKEN"));
    }

 

로그아웃 로직을 확인해보자. 

public void logout(String email, String accessToken) {
        long expiredAccessTokenTime = getExpiredTime(accessToken)
                .getTime() - new Date().getTime();

        redisService.deleteData(email);

        redisService.setBlackList(accessToken, "ACCESS-TOKEN", expiredAccessTokenTime);
    }

 

먼저 Redis에 에세스토큰을 저장한다. 이때, 에세스 토큰의 남은 만료시간을 계산해서 넣어주는게 가장 좋다. 그리고 리프래쉬 토큰을 찾아서 삭제한다.

 

생각을 다시 정리해보자. 

 

1. 로그아웃 요청을 보낸 사용자의 에세스 토큰의 남은 만료시간을 계산한다. 

굳이 남은 만료시간을 계산해서 저장하는 이유는 나중에 나온다. 

 

2. 리프래쉬 토큰을 삭제한다. 

로그인할 때 키 값을 사용자의 이메일로 하고 리프래쉬 토큰을 저장했다. 해당 코드는 다음과 같다. 

redisService.setData(user.getEmail(), refreshToken, REFRESH_TOKEN_VALID_TIME);

 

처음부분에서 이야기 했지만 로그아웃 하는 가장 빠른 방법은 그냥 토큰 자체를 삭제시키는 것이다. 리프래쉬 토큰은 애초에 서버가 저장하고 있었기 때문에 바로 삭제시키면 된다. 

 

3. 에세스 토큰을 Redis에 저장한다. 

이제 에세스 토큰을 Redis에 저장한다. 이 때, Redis에 저장할 때 키든 값이든 나중에 데이터가 존재하는지만 판단하면 되기 때문에 어느쪽에 토큰 값을 넣어도 상관없다. 여기선 키에 토큰값을 넣었지만 값에 넣어도 되고 값에 "ACCESS-TOKEN"이라고 되어있는 것도 아무 문자나 상관없다. 에세스 토큰을 Redis에 저장하는 이유와 데이터의 존재여부를 판단하는 이유는 나중에 나온다. 

 

그럼 여기서 궁금한 점이 3가지 있을 것이다. 

 

1. 굳이 토큰의 남은 만료시간을 계산해서 저장하는 이유가 있을까 ?

2. 에세스 토큰을 Redis에 저장하는 이유는 뭘까 ?

3. 데이터의 존재여부를 판단한다는 말이 무슨 말일까 ? 

 

위 질문들에 대한 답은 아래 코드에 있다. 아래코드는 토큰의 유효성을 검사하는 로직이다. 

@Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = jwtProvider.resolveToken(request).getValue();
        if (token != null) setAuthentication(token, request);
        filterChain.doFilter(request, response);
    }

 

위 코드에서 아래 코드를 추가해야 한다. 

redisService.getKey(token) == null

 

총 코드는 다음과 같다. 

@Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = jwtProvider.resolveToken(request).getValue();
        if (token != null && redisService.getKey(token) == null) setAuthentication(token, request);
        filterChain.doFilter(request, response);
    }

 

이제 위 3개의 질문들에 대한 답을 할 수 있겠다.

먼저 에세스 토큰을 Redis에 저장하는 이유부터 말하기 전에 Redis를 그냥 저장소라는 말로 바꿔 말하겠다. 에세스 토큰을 저장소에 저장하는 이유는 로그아웃을 요청한 사용자의 토큰만 저장소에 있을테니까 만약 저장소가 비어있다면 로그아웃을 하지 않은 것이다. 비어 있지 않다면 로그아웃을 했다는 소리다. 이렇게 저장소 안에 데이터가 존재하는지에 따라서 로그아웃을 했는지 안했는지 판단할 수 있다. 

 

굳이 토큰의 남은 만료시간을 계산해서 저장하는 이유는 효율적이기 때문이다. 만약 그냥 토큰의 만료시간을 넣으면 만료시간이 끝나고 데이터가 사라지지만, 남은 만료시간을 계산하면 조금 더 빨리 데이터가 삭제된다. 조금 더 빨리 삭제되는게 왜 효율적인지 생각해보자. 

 

위에 doFilterInternal 메서드는 사용자가 요청을 할 때마다 돈다. 게시글을 작성할 때도 돌고, 댓글 작성할 때도 돌고, 좋아요를 누를 때도 돌고.. 만약 수 천명의 사용자가 서비스를 이용한다면 저장소에 데이터가 쌓이고, 결국은 성능 이슈가 터지는 것이다. 그렇기 때문에 데이터가 더 빨리 삭제되는 것은 당연히 좋고, 저장소로 Redis를 많이 사용한다. 그 이유는 Redis가 인메모리 방식이기 때문에 다른 데이터베이스 보다 훨씬 빠르다. 하지만 결국은 요청이 올 때마다 Redis 안을 확인하는 것이기 때문에 아무래도 무리가 있을 수 있다. 

 

하지만 만약 로그아웃을 했는지 안했는지 확인하는 검사가 필요없다면 ? 지금쯤 아마 의아해 할 것이다. 이 블로그의 첫 문장을 다시 읽어보자. "토큰을 로그아웃 하기 위해서는 토큰의 만료시간을 만료시키거나 토큰 자체를 삭제해야 한다." 를 못해서 지금까지 Redis를 써가면서 어렵게 로그아웃 로직을 짠 것이다. 쿠키를 이용하면 훨씬 간단하면서도 성능이 개선될 수 있다. 

 

 

쿠키로 로그아웃

그럼 한번 구현해보기 전에 어떻게 쿠키를 이용하는지 결론부터 말하면, 토큰을 쿠키에 감싸서 관리하는 것이다. 한번 알아보자.

아래코드는 로그인 로직이다. 

    public TokenResponseDto login(LoginRequestDto loginReq) {
        User user = userRepository.findByEmail(loginReq.getEmail())
                .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

        user.matchedPassword(passwordEncoder, user, loginReq.getPassword());

        final String accessToken = jwtProvider.createAccessToken(loginReq.getEmail());
        final String refreshToken = jwtProvider.createRefreshToken(loginReq.getEmail());
        redisService.setDataExpire(loginReq.getEmail(), refreshToken, REFRESH_TOKEN_VALID_TIME);

        Cookie accessTokenCookie = cookieProvider.createCookie("ACCESS-TOKEN", accessToken, ACCESS_TOKEN_VALID_TIME);
        Cookie refreshTokenCookie = cookieProvider.createCookie("REFRESH-TOKEN", refreshToken, REFRESH_TOKEN_VALID_TIME);

        return TokenResponseDto.builder()
                .accessToken(accessTokenCookie)
                .refreshToken(refreshTokenCookie)
                .build();
    }

 

바뀐게 있다면 클라이언트한테 토큰을 바로 반환하는 것이 아니고 쿠키에 감싸서 보낸다. cookieProvider 코드는 다음과 같다. 아래 코드에 대해서 이해가 가지 않거나 궁금한 점은 여기를 참고하자. 요청에서 어떻게 쿠키를 가져오는지와 관리하는 것도 나와있다. 

@Component
public class CookieProvider {

    public Cookie createCookie(String name, String value, long time) {
        Cookie cookie = new Cookie(name, value);
        cookie.setHttpOnly(true);
        cookie.setMaxAge((int) time);
        cookie.setPath("/");
        return cookie;
    }

}

 

바로 로그아웃 요청으로 넘어가보겠다. 

    @DeleteMapping("/logout")
    public void logout(HttpServletRequest req, HttpServletResponse res) {
        LogoutResponseDto dto = authService.logout(req);
        res.addCookie(dto.getAccessToken());
        res.addCookie(dto.getRefreshToken());
    }

 

로그아웃 로직을 확인해보자. 

    public LogoutResponseDto logout(HttpServletRequest req) {
        Cookie accessToken = jwtProvider.resolveToken(req);
        Cookie refreshToken = jwtProvider.resolveRefreshToken(req);

        expireCookie(accessToken);
        expireCookie(refreshToken);
        
        return LogoutResponseDto.builder()
                .accessToken(accessToken)
                .refreshToken(refreshToken)
                .build();
    }
private void expireCookie(Cookie cookie) {
        cookie.setMaxAge(0);
        cookie.setPath("/");
    }

 

요청에서 에세스 토큰과 리프래쉬 토큰이 담긴 쿠키들을 가져온다음, 각각의 쿠키 만료시간을 0으로 표시함으로써 만료시켰다. 

쿠키를 만료시킴으로써 브라우저에 저장 되어있던 쿠키가 삭제된 것을 볼 수 있다. 

 

 

포스트맨에서 로그아웃 요청 전

 

 

로그아웃 요청 후

 

 

이렇게 하면 로그아웃을 했는지 안했는지 확인하는 검사가 필요가 없어지므로 성능이 올라간다. 로그아웃을 프론트에서 하는 방식도 있다. 프론트에서 바로 토큰을 지워주기만 하면 되니까 Redis나 쿠키를 사용한 방법보다 더 쉽다.

 

전체코드는 깃허브에 있으니 참고해주세요!

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday