accessToken은 변수(recoil)에 담았다면

refreshToken은 보안을 위해 담는 곳 옵션이 있다.
1. cookie에 담는다. (httpOnly-JS접근 불가)
2. secure에 담는다. (https://~ )

나는 쿠키에 담아보겠다!
쿠키의 특징 : api 요청할 때 같이 빨려들어간다. bearer 뭐시기 하지 않아도 된다.

그러면 이제 인증은 끝.

실질적으로 인가를 시작하려고 한다.

브라우저에서 Bearer AccessToken을 fetchProfile로 보내고 거기서 인가를 해서 열여 봤더니 성공하면 좋은데 토큰 만료가 떠버린 거라.
토큰 인가 실패! -> 토큰 만료 에러를 보냄 -> 그러면 원래는 브라우저에서 router.push를 해서 로그인 페이지 이동을 함. with 세션이 만료되었습니다. 재로그인 해주세요. 하지만 accessToken이 만료될 때마다 로그인을 하라고 하면 너무 번거롭다.

그래서! refreshToken을 이용하는 거다!

1. 토큰 만료 에러가 발생했는지 체크

브라우저에서 에러가 발생 했는지를 체크하는 거다. 그 중에서도 토큰 만료 에러가 발생했는지(모든 API 요청 할 때마다 체크한다.)
Axios는 요청할 때마다 체크 해주자(이거 어떻게 하는지 찾아보기)

2. RefreshToken을 가지고 AccessToken을 재발급

restoreAccessToken으로 재발급을 요청한다.

재발급 요청할 때 쿠키에 있는 refreshToken을 보낸다.-> 그걸 받아서 인가를 하는데 여기서는 refreshToken 인가를 한다. 인가를 했는데 refreshToken까지 만료가 된 거면 그건 로그인 한 번 해달라고 요청해야하는 부분임. 로그인을 너무 안 했다는 말이니까.->refreshToken 기한이 남아있으면 인가 성공! -> 그러면 새로운 AccessToken 하나 발급해 줄게 -> 그걸 다시 브라우저에 보내준다 -> 브라우저는 자신이 변수(globalStates recoil)에 갖고 있던 accessToken 부분을 새걸로 바꿔치기 한다. ->그러면 이제 다시 3번으로 넘어가!!
AccessToken을 중간에 탈취당하면 그걸 악용하기 때문에 기한을 짧게 잡은 것이다.


AccessToken이 새 거가 되었다!

3. 실패 쿼리 재시도

바꿔치기 하고 방금 실패했던 패치 프로필 쿼리에서 http 헤더, 바디 다 그대로 놔두고 토큰 부분만 새걸로 바꿔치기 함. bearer뒤에 바꿔치키 한 후에 fetchProfile로 보내고 인가하는 과정을 다시 시도함.


그러면 이번에 재시도 할 때는 인가에 성공!

그 이후에 프로필 정보를 조회해서 갖다주면 성공이겠죠?

정리하면

개발자의 입장

  1. 토큰 만료 에러를 캐치한다.
  2. 토큰을 재발급 받는다. (refreshToken 던져서 AccessToken을 재발급 받는다.)
  3. 기존 AccessToken 새거로 바꿔치기 해서 실패했던 쿼리를 재시도 한다.

유저의 입장

  1. 유저가 내 정보 보기 메뉴 클릭
    위 과정은 1초도 안 걸리기 때문에 유저는 뒤에서 어떤 과정이 있었는지 모른다.
    이런 것을
    silentAuthentication 이라고 부른다.

+추가 설명
위의 과정을 백엔드의 입장에서는 AuthService라고 부르는데 그 부분만 똑 떼어서 구글, 카카오, 네이버, 페이스북이 제공하는 걸 요즘 많이 볼 수 있다. 그 회사들에 저장된 DB를 이용할 수 있다. 소셜 로그인 (Open Authentication) 줄여서 OAuth라고 한다.


++백 단에서 요즘 AuthService, profileService, BoardService 이런 식으로 파트를 나누는데, 이거를 서버를 잘게 쪼갰다해서 MSA(Micro Service Architecture)라고 한다고 한다.

위에서는 쿠키에 담아보겠다고 했는데 일단은 세션에 담아보았다.

// 1. AccessToken 만료 후 인가 요청
// 2. 토큰 만료 에러가 발생했는지 체크
// 3. RefreshToken으로 AccessToken 재발급 요청
// 4. 발급 받은 AccessToken을  재저장
// 5. 방금 실패했던 쿼리 재시도



async function getNewTokenWithRefreshToken() {
  //local storage에서 refreshToken 읽어와서 변수에 담기
  const refreshToken: string | null = localStorage.getItem("refreshToken");
  console.log("여기까지는 잘 왔니?")
  console.log("리프레쉬 토큰입니다",refreshToken);

  // refreshToken이 null이라면 에러를 던지거나 적절한 처리를 수행
  if (!refreshToken) {
    console.error("No refresh token found in local storage");
    throw new Error("No refresh token found");
  }

  try {
    //이 refreshToken으로 새로운 accessToken 받아오기
    const response = await refreshingToken.post('/tokens', {},
    {
      headers: {
        accept: 'application/json',
        Authorization: `Bearer ${refreshToken}`, 
      },
    });
    console.log("========response.data.accessToken입니다:",response.data.accessToken);
    return response.data.accessToken;
  } catch (error) {
    console.error("Error while refreshing token", error);
    throw error;
  }
}

const MAX_RETRY_COUNT = 3;  // 임계값 설정

// 1. AccessToken 만료 후 인가 요청 -> 2. 토큰 만료 에러 발생
apiBasic.interceptors.response.use(
  (response) => response, // 성공한 요청에 대한 응답을 그대로 반환
  async (error) => {
    const originalRequest = error.config;

    // 초기값 설정. 만약 retryCount가 없으면 0으로 설정
    if (!originalRequest.retryCount) {
      originalRequest.retryCount = 0;
    }
    // 에러 상태가 401이면 토큰이 만료되었을 수 있음을 확인
    // '!originalRequest._retry' 갱신 후 재요청을 시도하는 것이 처음인지 아닌지 판단.
    // 만약 응답 코드가 401이고(인증 실패), 이전 토큰 갱신을 시도한 적이 없다면
    if (error.response.status === 401 && originalRequest.retryCount < MAX_RETRY_COUNT) {
      originalRequest.retryCount += 1;
      console.log("토큰이 만료여")
      //새로운 토큰을 이용한 요청이 또 다시 401을 반환한다면 어딘가 문제가 있다는 신호임.
      //그런 경우를 대비해서 _retry를 true로 설정함으로써 무한 재시도 루프에 빠지지 않게 함.
      //무한 루프 방지 방법임.
      // originalRequest._retry = true;

      // 새로운 accessToken 가져오기
      try {
// 3. RefreshToken으로 AccessToken 재발급 요청
        const accessToken = await getNewTokenWithRefreshToken();

        if (!accessToken) {
          console.error("Failed to refresh token");
          window.location.href = '/LoginPage';
          return Promise.reject(error);
        }

// 4. 발급 받은 AccessToken을 localStorage에 재저장
      localStorage.setItem("tokenForAdmin", accessToken);
      console.log("tokenForAdmin를 잘 저장했니?", accessToken);

// 5. 원래 요청의 인증 헤더를 업데이트하고 다시 시도
      originalRequest.headers.Authorization = `Bearer ${accessToken}`;
      console.log("헤더 업데이트를 잘 했니?", originalRequest.headers.Authorization);
      return apiBasic(originalRequest);
    } catch (err) {
      console.error("토큰 갱신 중 오류 발생:", err);
      // 토큰 갱신에 실패하면 에러를 반환하고 재시도하지 않음
      return Promise.reject(err);
    }
  }

    // 에러가 다른 이유 때문이라면, 다음 핸들러에게 에러를 던진다
    return Promise.reject(error);
  }
);
profile
느리더라도 꾸준히 확실하게.

0개의 댓글