refreshToken으로 권한 분기하기

이주희·2022년 5월 2일
2

React ♥️ Next.js

목록 보기
40/48

로그인을 하지 않은 사용자가 이용할 수 없는 페이지에 접근하면
"로그인 후 이용해주세요." 라는 Alert과 로그인 화면으로 이동시키는 HOC를 만들어보자!

사실 전에 만들었는데 잘 안돼서 ㅋ_ㅋ A/S 고고

보안 목적 상 accessToken은 state에 담아놓고, accessToken을 재발급 받을 수 있는 refreshToken이 쿠키에 저장되어 있는 상황이다.

accessToken은 state에 저장되어 있기 때문에 새로고침을 할 때마다 state는 초기화되어 accessToken은 없는 상태가 된다!!
그래서 useEffect 안에서 accessToken을 재발급 받는 함수를 통해 accessToken을 재발급 받아오도록 하고, 재발급 받는 함수를 요청했는데도 accessToken이 없으면(refreshToken이 만료되었거나, 로그아웃을 해서 refreshToken이 없는 경우) 권한이 없다고 판단하고 로그인 화면으로 보내도록 할 것이다.

1. accessToken을 발급하는 함수

src > commons > libraries
getAccessToken

import { GraphQLClient, gql } from "graphql-request";

// accessToken을 재발급해주는 쿼리
const RESTORE_ACCESS_TOKEN = gql`
  mutation restoreAccessToken {
    restoreAccessToken {
      accessToken
    }
  }
`;

export async function getAccessToken() {
  /* 2-1. refreshToken으로 accessToken을 재발급 받는다. */
  try {
    const graphQLClient = new GraphQLClient(
      "https://backend06.codebootcamp.co.kr/graphql",
      { credentials: "include" }
    ); // endpoint를 넣어준다. https로 넣어줘야 한다!!!
    const result = await graphQLClient.request(RESTORE_ACCESS_TOKEN); // 쿼리를 넣어준다.
    const newAccessToken = result.restoreAccessToken.accessToken;
    return newAccessToken;
  } catch (error) {
    if (error instanceof Error) console.log(error.message);
  }
}

2. 토큰을 받아와서 globalState에 넣어주기

src > commons > store
store

import { atom } from "recoil";
import { getAccessToken } from "../libraries/getAccessToken";

export const accessTokenState = atom({
  key: "accessTokenSate",
  default: "",
});

src > components > commons > apollo
ApolloSetting

  const [accessToken, setAccessToken] = useRecoilState(accessTokenState);
  useEffect(() => {
    /* accessToken 재발급 받아서 state에 넣어주기 */
    getAccessToken().then((newAccessToken) => {
      setAccessToken(newAccessToken);
    });
  }, []);

3-1. 토큰이 없으면 로그인 화면으로 이동시키는 HOC

src > components > commons > hocs
withAuth

import { useRouter } from "next/router";
import { useEffect } from "react";
import { useRecoilState } from "recoil";
import { getAccessToken } from "../../../commons/libraries/getAccessToken";
import { accessTokenState } from "../../../commons/store";
// @ts-ignore
export const withAuth = (Component) => (props) => {
  const [accessToken] = useRecoilState(accessTokenState);

  const router = useRouter();

  useEffect(() => {
    if (!accessToken) {
      getAccessToken().then((newAccessToken) => {
        if (!newAccessToken) {
          alert("로그인 후 이용해주세요.");
          router.push("/login");
        }
      });
    }
  }, []);

  return <Component {...props} />;
};
// Component가 화면에 실행된다.
// 결과는 똑같이 Component가 화면에 보이지만, 다른 점은 중간에 logic이 추가된 것이다.

위 방식은 요청이 getAccessToken에서도 이루어지고, 이 HOC에서도 이루어져서
총 두번 이루어지므로 비효율적이다.

(HOC에서는 요청을 안 보내게 하면
getAccessToken을 해서 받아오는 동안에 accessToken이 없어서
withAuth에서 권한이 없다고 판단되어 로그인 화면으로 이동해버린다...)

3-2. 글로벌 프로미스 방식 HOC

  • 불필요한 중복 요청을 없애기 위해서, getAccessToken 함수 자체를 글로벌로 빼자!
  • useRecoilValueLoadable을 활용해서 state가 요청중인지 요청이 끝났는지 상황을 알 수 있다.
  • 글로벌함수로 만들면 요청을 한 곳이 어디든간에 함수가 있는 곳에서 모두 실행이 되고 값을 돌려받게 된다.

(1) 글로벌 함수 만들기

recoilState를 만들 때, atom은 특정 데이터를 저장하는 반면 selector는 기능이 들어갈 수 있다.
글로벌 함수처럼 쓸 수 있게 된다!!

src > commons > store

import { selector } from "recoil";
import { getAccessToken } from "../libraries/getAccessToken";

// atom은 특정 데이터, selector는 기능이 들어갈 수 있다.
// 이 함수를 여러 컴포넌트에서 공유할 수 있게 된다. like 글로벌 함수!
export const restoreAccessTokenLoadable = selector({
  key: "restoreAccessTokenLoadable",
  get: async () => {
    const newAccessToken = await getAccessToken();
    return newAccessToken;
  },
});

(2) 글로벌 함수로 토큰 재발급 받기

src > components > commons > apollo
ApolloSetting

  const restoreAccessToken = useRecoilValueLoadable(restoreAccessTokenLoadable);

  useEffect(() => {
    restoreAccessToken.toPromise().then((newAccessToken) => {
      setAccessToken(newAccessToken);
    });
  }, []);

(3) 권한 분기에 사용하기

src > components > commons > hocs
withAuth

  useEffect(() => {
    if (!accessToken) {
      restoreAccessToken.toPromise().then((newAccessToken) => {
        if (!newAccessToken) {
          alert("로그인 후 이용 가능합니다.");
          router.push("/login");
        }
      });
    }
  }, []);
profile
🍓e-juhee.tistory.com 👈🏻 이사중

0개의 댓글