Refresh Token, 블랙 리스트 도입기

손효재·2022년 11월 23일
3
post-thumbnail

토큰 기반의 인증 방식을 선택한 이유와 사용

프로젝트를 진행하면서, 사용자의 인증 여부를 확인하기 위해 JWT 토큰 기반의 방식을 선택했다.
서버의 자원을 사용하지 않고 클라이언트에 보관하여 stateless 하다는 특징과 어떤 서버로 요청을 보내도 해당 토큰이 유효한지 확인하면 되는 확장성에 용이한 점을 이유로 들었다.

처음 사용하면서 access token 만으로 인증 방식을 진행했지만, access token을 탈취당했을 상황의 문제점에 대해 고려하면서 refresh token을 도입하여 보완하고, 로그아웃 이후의 탈취상황을 고려해 블랙리스트(Blacklist)라는 개념도 도입해봤다.

기존 Access Token 로직

  1. 클라이언트가 로그인을 요청하면, 서버에서 access token을 생성하여 반한한다.
  2. 클라이언트에서 access token을 로컬/세션 스토리지에 저장한다.
  3. 유저 인증 필요시 access token을 Request header에 담아 요청을 보낸다.
  4. 서버에서 토큰을 인증 및 유효시 응답한다.

이때 클라이언트에 노출되어 비교적 탈취당하기 쉬운 access token의 특성상 유효기간을 짧게 30분 정도로 가져갔다. 이로인해, 30분 마다 만료되는 access token을 재발급 받기위해 사용자는 반복적으로 로그인을 해야하는 문제점이 생긴다. 또한, 단일 토큰으로 access token만 사용한다면, 해당 토큰이 탈취당했을 때의 위험도 크다.
이를 보완하기 위해 서버측에서 별도로 보관하기 위한 refresh token을 추가했다.

Refresh Token 로직

  1. 클라이언트 로그인 요청,
  2. 서버에서 access token, refresh token 생성 및 반환
    access token의 유효기간을 짧게, refresh token을 길게 가져간다.
    ex) access token 30분, refresh token 2주
    * 이때, 서버에서 refresh token은 Redis를 사용해 저장했다.
    레디스의 유효기간을 설정해서 관리할 수 있는 점과, 유저 식별자를 키값으로 저장하여 쉽게 저장할 수 있는 이유로 선택했다.
  3. 클라이언트에서 access token은 로컬/세션 스토리지, rrefresh token은 쿠키에 저장한다. (HttpOnly 설정)
  4. 인증이 필요한 요청시 기존과 동일하게 access token을 header에 담아서 요청한다.

refresh token을 도입하여 access token의 유효기간을 짧게하여 보안도 높이면서 편의성도 챙길 수 있게 되었다.

  1. access token이 만료됐다면, refresh token으로 access token 재발급을 요청한다.
  2. refresh token이 만료됐다면, 로그인을 재요청한다.

Access Token이 만료된다면

이제 클라이언트에서 header에 담아 보낸 access token의 유효기간이 만료된 상황에는 어떻게 동작할까?

  1. 서버에서 access token이 만료되었다는 에러를 반환한다.
  2. 클라이언트는 refresh token으로 access token의 재발급을 요청한다.
  3. 서버에서 refresh token 비교 및 인증 후 access token 재발급하여 반환한다.
  4. 클라이언트는 access token을 저장하여 사용한다.

이처럼 만료된 access token으로 에러가 반환되고, refresh token을 다시 담아서 재발급을 요청하는 과정이 추가되어야 한다.

Access Token 만료시, 재요청 과정을 개선할 수 있는 방안

하지만, 위와 같은 클라이언트와 서버사이의 통신이 비효율적으로 생각되었다.
서버까지의 통신과정을 조금이라도 해소하기위해, 클라이언트에서 항상 요청을 보내기 전에 access token이 만료된 토큰인지 확인한다면 어떨까?

* 클라이언트에서 Access token 만료 확인방법

  1. base64 Decode를 사용
    - jwt-decode를 import해서 토큰을 decode하여 유효기간을 확인한다.
import jwt_decode from "jwt-decode";
const decode = jwt_decode(sessionStorage.getItem("access-token"));
  1. Access Token을 클라이언트에 저장할때 토큰의 유효기간을 함께 저장한다.
    - 저장된 Access token의 유효기간을 체크하여 만료된 토큰인지 판단한다.

두 가지 방법을 활용하여 클라이언트에서 요청을 보내기 전, access token의 만료여부를 확인하고 필요한 요청을 보내는 것이다. 토큰이 만료되었다면, 서버로 refresh token을 보내 새로운 access token을 발급받을 수 있는 요청을 바로 보낸다면, 기존에 access token의 만료 에러를 받고 재발급을 요청하는 1번의 과정을 생략할 수 있겠다고 생각했다.

Blacklist 개념 도입

유저가 서비스 사용을 마치고 로그아웃하면서 저장된 access token을 삭제하도록 했다. 하지만, access token의 유효기간은 여전히 남아있고 이를 누군가에게 탈취당한다면 어떻게 될까??

유저가 로그아웃하여 access token과 refresh token을 삭제하겠지만, 아직 유효기간이 남아있는 access token이 탈취당해 짧은 유효기간이지만 해당 시간동안 문제가 발생할 것이라 생각했다.

이를 위해 블랙리스트 개념을 도입하여, 유저가 로그아웃시 access token의 남은 유효기간만큼 Redis에 유효기간을 설정하여 블랙리스트로 등록해놓는다. 유효기간이 끝나면 자연스럽게 Redis에서 삭제시켜 메모리에 남아있지 않도록 해준다.

블랙리스트(Blacklist) 로직

  1. 유저가 로그아웃 요청과 함께 access token을 서버에 담아 보낸다.
  2. Redis의 블랙리스트에 access token을 남은 유효기간만큼 설정하여 저장한다.

이렇게되면, 사용자가 서비스 사용을 끝냈지만, 아직 유효기간이 끝나지않은 토큰을 Redis의 블랙리스트에 저장하고, 모든 클라이언트 요청이 들어올 때 Redis의 블랙리스트를 조회하도록 한다.
블랙리스트에 존재하는 토큰으로 인증 시도가 들어오면, 사용이 끝난 유저의 토큰이므로 누군가 탈취하여 사용하려는 의도로 판단하고 요청을 거부하도록 한다.

최종 로직

최초 Token 생성시

  1. 사용자 로그인 요청
  2. 서버에서 access token, refresh token 생성해 반환하고, 서버는 refresh token을 Redis에 저장한다.
  3. 클라이언트에서 access token은 로컬/세션 스토리지, rrefresh token은 쿠키에 저장한다. (HttpOnly 설정)

클라이언트 요청시

  1. 요청에 담아 보낼 access token의 유효기간을 확인한다.
  2. 유효한 토큰이면 Request header에 담아 요청을 보낸다.
    유효기간이 지난 토큰이면 refresh token을 담아 access token의 재발급을 요청한다.
    서버는 refresh token이 유효하면 access token을 재발급한다.
    2-1. 재발급된 access token을 재저장하고, 기존 요청을 다시 보낸다.
  3. 클라이언트가 요청한 access token이 Blacklist에 등록되었는지 Redis를 조회하여 확인한다.
    3-1. 블랙리스트에 저장된 토큰이라면 에러를 반환한다.
  4. 블랙리스트에 등록되어있지 않고, access token이 유효하다면, 응답을 반환한다.

로그아웃시

  1. 사용자 로그아웃 요청
  2. 클라이언트는 로컬/세션 스토리지에 저장된 access token을 삭제한다.
  3. 서버에 access token을 담아 보낸다.
  4. 서버는 Redis에 저장된 해당 유저의 refresh token을 삭제하고, 해당 access token을 Redis의 블랙리스트로 추가한다.

느낀점

보안과 관련된 내용은 항상 어렵게 느껴진다..
그래도 JWT 토큰을 사용하면서 다양하게 생각해본 시간이 되었고, 블랙리스트라는 개념도 도입해보면서 보안적으로 좀 더 안전하게 사용하기 위해 노력했다고 생각된다.

0개의 댓글