로그인 구현 - 리프래쉬 토큰과 엑세스토큰

eggMun·2023년 3월 20일
0

나는 로그인을 구현하려고 한다.

  const onSubmit = async (data: FormValues): Promise<void> => {
    const { email, password } = data;
    await login({ variables: { email, password } });
  }
  1. 먼저 로그인 요청 api를 작성한다.
    그러면 서버에서 리프래쉬 토큰과 엑세스 토큰을 발급해 준다.
const onSubmit = async (data: FormValues): Promise<void> => {
    const { email, password } = data;
    const result = await login({ variables: { email, password } });
    const accessToken = result.data.login;
    console.log(accessToken);
    if (accessToken === undefined) {
      alert("로그인에 실패하였습니다. 다시 시도해주세요!");
      router.push("/")
      return;
    }
    setAccessToken(accessToken);
    alert("로그인 되었습니다.");
  };

api 요청을 result라는 변수에 담는다.
그러면 reuslt에 엑세스 토큰이 담긴다.
그리고 새 accessToken 변수를 만들어 result.data.login인 즉 엑세스 토큰을 담는다.
그런 다음에는 글로벌 변수인 recoil을 통해서 setAcceessToken에 담는다.
그러면 엑세스 토큰을 어디서든지 사용이 가능하게 된다.
그리고 예외처리도 하나 하였다.
만약에 엑세스 토큰이 언디파인드라면 로그인에 실패한거므로 메인 홈으로 보냈다.

여기까지 엑세스 토큰을 글로벌 스테이트에 담는거 까지 하였다!!

  1. 그 다음은 엑세스 토큰이 1시간 만료이므로 1시간 뒤에는 자동으로 재발급 받는 코드를 작성해야 한다.
    먼저 아폴로 세팅을 해준다.
 const uploadLink = createUploadLink({
    uri: "https://api.upco.space/main",
    headers: {
      Authorization: `Bearer ${accessToken}`,
    },
    credentials: "include",
  });

토큰 요청은 Authorization필드에 담아져 보내지게 된다.
그래서 우리 팀프로젝트는 JWT 토큰 방식으로 로그인을 하기 때문에 토큰 요청시 Bearer를 포함한 접두어와 토큰을 같이 보내게 된다. 그래서 설정을 해둔거다.
그리고 credentials옵션에서 "include" 설정을 해준다.
그러면 로그인이 필요한 api 요청을 보낼 때 쿠키 정보도 같이 보낼 수 있어 서버에서 확인이 가능하다!

  1. 새 엑세스 토큰 발급받기!
const RESTORE_ACCESS_TOKEN = gql`
  mutation {
    restoreAccessToken
  }
`;
export const getAccessToken = async (): Promise<string | undefined> => {
  try {
    const graphQLClient = new GraphQLClient("https://server.link", {
      credentials: "include",
    });
    const result = await graphQLClient.request(RESTORE_ACCESS_TOKEN);
    const newAccessToken = result.restoreAccessToken;
    console.log("새 엑세스토큰", newAccessToken);
    return newAccessToken;
  } catch (error) {
    if (error instanceof Error) console.log(error.message);
  }
};

먼저 새 엑세스 토큰 발급받는 api 요청을 작성한다.
그리고 request를 사용하기 때문에 다시 아폴로 세팅을 해주어야 한다.
그리고 리퀘스트로 새 엑세스토큰을 발급 받는다.
마지막으로 getAccessToken라는 변수에 담아서 이 함수를 사용한다!!

  1. 일정 조건에서만 새 엑세스 토큰 발급 받게하기
 const errorLink = onError(({ graphQLErrors, operation, forward }) => {
    if (typeof graphQLErrors !== "undefined") { // A
      for (const err of graphQLErrors) { // B
        if (err.extensions.code === "UNAUTHENTICATED") { // C
          return fromPromise( // D
            getAccessToken().then((newAccessToken) => { // E
              setAccessToken(newAccessToken ?? ""); // F
                operation.setContext({ // G
                headers: { 
                  ...operation.getContext().headers, // H
                  Authorization: `Bearer ${newAccessToken}`, // I
                },
              });
            })
          ).flatMap(() => forward(operation)); // J
        }
      }
    }
  });```
먼저 아폴로 클라이언트에서 제공해주는 onError 메소드를이용하여 에러를 캐치한다.
onError에는 매개 변수로 오류 핸들러 함수를 넣을 수 있다.
그리고 그 자리에 graphQLErrors를 넣어 준다.
graphQLErrors은 서버에서 발생한 오류를 배열로 알려준다.
또 다른 매개 변수인 operation은 객체에는 현재 실행중인 연산의 모든 정보가 포함되어 있다.
그리고 forwar는 실패한 api 요청을 다시 보내주는 역활을 한다
이제부터 저 위 코드의 알파벳 주석으로 설명하겠다.
A: graphQLErrors를 이용하여 에러를 배열로 받았으므로 에러가 있을 경우에만 이 함수를 실행시킨다.
B: graphQLErrors는 배열이다 배열안에 err객체들이 있다. 그것을 반복문으로 돌리는거다.
C: 그리고 해당 에러가 토큰 만료인지 체크하는거다. "UNAUTHENTICATED"
D: 그 에러가 맞다면 리턴한다.
E: 3번에서 만든 getAccessToken() 함수를 이용하여 새 엑세스 토큰을 발급 받는다.
F: 그리고 글로벌 스테이트인 setAccessToken에 새로운 엑세스 토큰을 넣어준다.
G: 그리고 실패한 api 요청을 변경시킨다.
H: 방금 실패한 api 요청의 헤더를 가져온다. 여기까지는 만료된 토큰이 Authorization 필드에 담겨져 있다.
I: 실패한 api 헤더를 가져왔으므로 새 엑세스 토큰으로 교체한다!
J: 그리고 방글 수정한 쿼리를 재요청한다 즉 api 요청을 재요청한다.
  1. 새로고침 시 엑세스 토큰이 날라가는 문제 해결
    위 방식으로 진행하면 글로벌 스테이트에 엑세스 토큰을 담고 있기 때문에 새로고침을 하면 글로벌 스테이트가 초기화 되기 때문에 엑세스 토큰이 날라간다.
    그래서 useEffect로 작업을 해주어야 한다.
export const restoreAccessTokenLoadable = selector({
  key: "restoreAccessTokenLoadable",
  get: async () => {
    const newAccessToken = await getAccessToken();
    return newAccessToken;
  },
});

일단 글로벌 스테이트로 selector을 사용해 get값을 전달해 주려고 한다.
왜 이렇게 했나면 getAccessToken함수의 리턴값인 새 엑세스 토큰을 글로벌로 쓰기 위해서다.
즉 나중에 권한분기에 적용하기 위해 미리 작업을 해두었다.

  const refresh = useRecoilValueLoadable(restoreAccessTokenLoadable);
  useEffect(() => {
    void refresh.toPromise().then((newAccessToken) => {
      setAccessToken(newAccessToken ?? "");
    });
  }, []);

그리고 위에서 사용한 selector을 가져와서 새로고침 시 새 엑세스 토큰을 발급 받도록 하였다!!

이렇게 로그인은 끝이다~~
1번부터 5번까지의 과정으로 긴 과정이지만 처음에 이해하는 것도 어려웠고, 바로바로 적용하는게 어려웠다.
하지만 공부를 하면서 하나하나 이해를 하니 이제 로그인에 대해서 이해를 하였다!!

profile
블로그 이전: https://eggmun98.tistory.com/

0개의 댓글