[Spring] Redis를 이용한 로그아웃 구현

박성우·2023년 6월 21일
0

Spring

목록 보기
6/10

JWT 인증 유저가 로그아웃을 하려면 기본적으로 해야할 것이 무엇일까?

클라이언트가 로그인할 때 발급받은 Access Token, Refresh Token을 제거하는 것이 1순위다.

그런데, 유저 인증에 사용되는 Access Token은 유효 기한이 끝나기전에 탈취당하면 아무리 삭제했다한들 여전히 인증이 가능하다.

따라서, Access Token을 남은 유효 기한과 함께 저장소에 저장해두다가 해당 Access Token의 유효 기한이 끝날 때까지 인증을 막아서 보안을 강화할 수 있다.

단순히 로그아웃 시
1. 남은 유효 기한과 함께 Access Token 저장 (Blacklist 등록)
2. 인증 시도 시 Access Token이 Blacklist에 등록되어있는 지 확인
이 전부다.

❓저장소로 Redis를 사용할 것 인데, 굳이 Redis를 사용하는 이유가 무엇일까

RDB로도 구현하려면 할 수는 있겠지만, In-Memory 저장소의 데이터 조회 속도 / TTL(Time To Live)을 통한 데이터 자동 삭제 기능만으로도 Access Token을 다루는 Blacklist를 구현하기에 상대적으로 매우 적합한 것으로 보인다.

Redis 설정은 생략하고, 서비스 로직부터 보면

public ResponseDto<String> logout(HttpServletRequest request, Member member) {
    String accessToken = jwtUtil.resolveToken(request, JwtUtil.ACCESS_TOKEN);
    // 로그아웃 하고 싶은 토큰이 유효한 지 먼저 검증하기
    if (!jwtUtil.validateToken(accessToken)) {
        throw new CustomException(INVALID_TOKEN);
    }

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

    // 해당 Access Token 유효시간을 가지고 와서 BlackList에 저장하기
    redisTemplate.opsForValue().set("BL:" + accessToken, "", jwtUtil.getRemainingTime(accessToken), TimeUnit.MILLISECONDS);

    return ResponseDto.setSuccess("로그아웃 성공");
}

Redis 조작에는 RedisTemplate을 사용하고 String 자료 구조를 이용하였다.

현재 Refresh Token 또한 Redis에 저장하고 있기 때문에 우선 저장되어 있는 Refresh Token을 삭제하고, Access Token은 남은 유효 기간을 TTL 설정과 함께 저장한다.

Refresh Token의 경우 Key 값에 "RT" 문자열과 유저의 Email(중복 X) 값을 조합해서 등록했지만, Blacklist의 경우 유저 구분이 필요없기 때문에, Key 값에 "BL" 문자열과 토큰을 조합해서 저장하고 Value는 비워두고 저장한다.

남은 유효 기간은 다음과 같이 계산한다.

// 토큰의 남은 유효시간을 반환
public long getRemainingTime(String token) {
    long expirationTime = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody().getExpiration().getTime();
    long currentTime = new Date().getTime();
    return expirationTime - currentTime;
}

인자로 들어온 토큰을 파싱해서 만료 기한을 알아내고, 현재 시간을 빼서 남은 유효 기간을 반환한다.

마지막으로, 유저가 인증을 시도할 때 해당 Access Token이 Blacklist에 등록되어 있는지만 확인하면 끝이다.

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

    String accessToken = jwtUtil.resolveToken(request, JwtUtil.ACCESS_TOKEN);

    if(accessToken != null) {
        // Access 토큰 유효 시, security context에 인증 정보 저장
        if(jwtUtil.validateToken(accessToken)) {
            // Redis에 해당 accessToken logout 여부를 확인
            String isLogout = (String) redisTemplate.opsForValue().get("BL:" + accessToken);
            // 로그아웃이 없는(되어 있지 않은) 경우 해당 토큰은 정상적으로 작동하기
            if (ObjectUtils.isEmpty(isLogout)) {
                setAuthentication(jwtUtil.getUserInfoFromToken(accessToken));
            }
        } else {
            jwtExceptionHandler(response, "Access Token Expired", HttpStatus.FORBIDDEN.value());
            return;
        }
       // Refresh Token를 통한 Access Token 재발급을 Http 요청에 의해 따로 처리
    }
    filterChain.doFilter(request,response);
}

인증이 이뤄지는 Securty Filter 내부에서 해당 Access Token이 Blacklist에 등록되어 있는 지 확인하고 등록되어 있지 않은게 확인되면 SecurityContextHolder에 유저 Authentication을 저장하지만, 등록되어 있다면 인증에 실패한다.

profile
Backend Developer

0개의 댓글