ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [graphql] 토큰만료시 연장처리
    카테고리 없음 2023. 6. 29. 08:43
    반응형

    accesstoken 만료이후에 refresh를 통해서 토큰을 연장해줄수 있다. 보통 refresh_token 을 해당용도로 사용하는데 기존 access_token을 통해서 새로운 refresh_expire time 이전이라면 refresh 할수 있도록 구성 할수도 있는거 같다.

     

    기존 코드는 interval timer를 통해서 만료시간이후에 토큰을 갱신시켜주고 있었다 
    대부분의 케이스에서 문제는 없었지만 interval 타임내의 동작이 유발될 경우 에러가 있었고 그게 아니더라도 백그라운드에서 해당체킹이 실행되고 있었기 때문에 개선이 필요한 부분이였다.

     

    axios를 사용해서 데이터를 받아오는 경우 intercepter 개념을 통해서 요청이전에 체크및 헤더삽입과 같은 부분을 처리할수 있었기 때문에 apollo-client(graphql) 에서도 비슷한 기능이 있겠거니 했다.

    https://www.apollographql.com/docs/react/api/link/introduction/

     

    apollo-client 에서는 client와 graphql server 사이의 커스터마이징을 하기위해서 Link를 사용하도록 했다.

    import { ApolloClient, InMemoryCache, from } from '@apollo/client';
    import httpLink from './httpLink';
    import errorLink from './errorLink';
    import authLink from './authLink';
    
    const client = new ApolloClient({
      link: from([authLink, errorLink, httpLink]),
      cache: new InMemoryCache(),
      credentials: 'include',
    });
    
    export default client;

    authlink에서는 요청시 헤더에 토큰을 넣어주는 부분 이였고 errorLink는 응답시 에러처리부분 코드 그리고 httpLink부분은 graphql server url 을 넣어주는 부분 이다.

     

    인터셉터 개념처럼 사용하기 위해서는 authlink에서 토큰만료시간 체크이후 만료됬다면 리프레시 이후에 해당 토큰으로 갈아서 헤더에 넣어 요청을 넣게 되면 된다.

     

    근데 그게 그당시에 잘 안됬다 authlink 파일을 보자

    import { setContext } from '@apollo/client/link/context';
    
    const authLink = setContext((_, { headers }) => {
      const token = localStorage.getItem('ACCESS_TOKEN');
     
      return {
        headers: {
          ...headers,
          authorization: token ? `Bearer ${token}` : '',
        },
      };
    });
    
    export default authLink;

    일단 setContext안에서 상식적으로 비동기 요청(리프레시) 이후에 새로운값으로 set 및 헤더삽입 을 했는데 뭐가 문제 였는지 잘 모르겠다.

     

    그래서 errorlink에서 처리를 해보는 걸로 경로를 변경했다. unauthorized 응답시 리프레시이후에 해당요청을 재요청하는 방법이다. 언뜻생각해도 비효율적이다. 요청이전에 만료시간을 가지고 있는데 해당값을 활용하지 못하고 오류받고 처리를 하기 때문이다. 

    그래서 이방법으로 해당처리를 하는것을 좋지 못하다고 생각한다. 근데 굳이 적어두는 이유는 onError 에서 에러처리를 하는 부분 문법이 상당히 어색하게 느껴졌고 찾기 힘들었다. 그래서 기록용으로.. 남겨둔다.

    import { onError } from '@apollo/client/link/error';
    import { fromPromise, ApolloClient, InMemoryCache, from } from '@apollo/client';
    
    const renewTokenApiClient = new ApolloClient({
      link: from([httpLink]),
      cache: new InMemoryCache(),
      credentials: 'include',
    });
    
    let isRefreshing = false;
    let pendingRequests = [];
    
    const resolvePendingRequests = () => {
      pendingRequests.map((callback) => callback());
      pendingRequests = [];
    };
    const setIsRefreshing = (value) => {
      isRefreshing = value;
    };
    const addPendingRequest = (pendingRequest) => {
      pendingRequests.push(pendingRequest);
    };
    
    const refreshToken = async () => {
      const token = getAccessToken();
    
      const {
        data: {
          refreshToken: {
            access_token: accessToken,
            expires_in: expiresIn,
            refresh_expires_in: refreshExpiresIn,
          },
        },
      } = await renewTokenApiClient.mutate({
        mutation: REFRESH_TOKEN,
        context: {
          headers: {
            Authorization: `Bearer ${token}`,
          },
        },
      });
    
      return { accessToken, expiresIn, refreshExpiresIn };
    };
    
    const errorLink = onError(({ graphQLErrors, networkError, forward, operation }) => {
      if (graphQLErrors?.[0].message === ErrorMessages.AUTHORIZATION_ERROR) {
        /*
        인증에러인 경우 access_token 을 통한 갱신 요청 작업
        인증에러가 복수인 경우가 있을수 있어 isRefreshing으로 체크후 갱신
        onError에서 비동기요청이 필요한 경우 fromPromise에 콜백으로 사용
        */
        if (!isRefreshing) {
          setIsRefreshing(true);
    
          return fromPromise(
            refreshToken()
              .then(({ accessToken, expiresIn, refreshExpiresIn }) => {
                setAccessToken(accessToken);
                setExpiredAt(addSeconds(expiresIn));
                setRefreshExpiredAt(addSeconds(refreshExpiresIn));
              })
              .catch(() => {
                removeAccessToken();
                removeExpiredAt();
                removeRefreshExpiredAt();
    
                resolvePendingRequests();
                setIsRefreshing(false);
    
                window.location.reload();
    
                return forward(operation);
              }),
          ).flatMap(() => {
            resolvePendingRequests();
            setIsRefreshing(false);
            return forward(operation);
          });
        }
    
        return fromPromise(
          new Promise((resolve) => {
            addPendingRequest(() => resolve());
          }),
        ).flatMap(() => forward(operation));
      }
    });
    
    export default errorLink;

    지금도 완전히 이해는 못하고 있는 코드다 forward, flatMap, fromPromise 등의 역할을 명확하게 이해하지 못하기 때문이라고 생각한다. 몇번 읽어봤는데 이해 안되서 나중에 보면 되겠지 생각하고 적어둔다. 에러가 동시에 여러곳에서 나는 경우 때문에 코드가 더 복잡해 진경우이긴 한데 그부분은 보다보면 이해는 간다. 

     

    일단은 기록용으로 적어뒀고 이코드를 다시 authlink에서 처리를 해주도록 변경해야한다. 해당 부분 되면 다시 써야지

Designed by Tistory.