30일차 / Refresh Token, Graphql

김혜진·2022년 4월 25일
0

7주차 커리큘럼

  • refresh Token
  • Graphql
  • Promise.all
  • Memoization
  • @Media

Refresh Token

브라우저에서 이메일과 패스워드를 백엔드에 넘겨주면 JWT토큰을 만들어 다시 브라우저에 전송 (accessToken)
브라우저에서는 브라우저 스토리지 (ex 로컬 스토리지, state 등)에 저장
스테이트에 저장하면 새로고침 했을 때 날아가기 때문에 로컬 스토리지에 저장을 해뒀었다.
하지만 로컬스토리지에 저장을 해두면 토큰이 탈취당할 수 있기 때문에 좋지 않은 방법이라고 배웠다.
그래서 이제는 state에 저장을 해보는 방식으로 해보자.

뮤테이션을 했을 때 액세스 토큰을 헤더에 Authorization을 키로 Bearer 액세스 토큰을 보내줬었다.
백엔드에서는 철수가 맞는지, 로그인 시간 남았는지를 확인하고 등록하고 브라우저에 응답을 준다.

토큰을 받아내는 과정 : 인증(Authentication)
토큰을 확인하는 과정 : 인가(Authorization)

토큰만 해킹을 하면 헤더에 토큰 정보만 추가하면 철수 행세를 할 수 있기 때문에 만료시간을 짧게 가져간다.
때문에 액세스 토큰이 만료가 되면 다시 로그인을 해야했기 때문에 불편하다.

그래서 토큰이 만료가 되면 새롭게 발급받을 수 있는 리프레시 토큰이 나왔다.

브라우저에서 로그인을 했을 때, login 토큰을 두개 만든다. (ex 액세스 토큰, 리프레시 토큰)
액세스 토큰은 payload(원래 데이터를 받아오던 곳)로 받고, 리프레시 토큰은 cookie(로컬,세션 스토리지로 받지 않게)로 받는다.
쿠키에는 httpOnly 옵션이 있어서 여기로 받는다.
httpOnly는 자바스크립트로 건드릴 수 없다.
리프레시 토큰을 탈취당하면 문제가 생길 수 있기 때문에 쿠키를 통해 받아온다.
secure옵션도 줄 수가 있는데, secure를 넣으면 https에서만 받아올 수 있다.
그러면 브라우저에서는 토큰을 건드릴 방법이 없다.

뮤테이션을 하는데 백엔드에서 인가를 하는데 토큰만료가 뜬다면 ? 브라우저에 에러를 던진다.
에러 이름 UNAUTHENTICATED이 뜬다. (=토큰 만료)
원래 같으면 우리는 로컬 스토리지에 가서 액세스 토큰을 지우고 액세스 토큰을 지우고 다시 로그인 했었다.
하지만 이제 브라우저에 가서 restoreAccessToken을 요청한다. 여기에는 refreshToken을 넣어준다.
쿠키에 저장된 데이터는 백엔드에 요청할 때 자동으로 첨부가 되어서 넘어간다.

이 API에서는 리프레시 토큰이 만료가 됐는지 안됐는지 비교를 한다. 리프레시 토큰까지 만료가 됐다면 로그인 페이지로 리다이렉트를 시킨다.
하지만 남아있다면 새로운 JWT토큰을 하나 더 만든다. 그 JWT토큰을 브라우저에 돌려준다. (accessToken)

그리고 다시 방금 전에 실패했던 API를 요청한다. 이번엔 인가가 되어서 데이터베이스에 등록을 하게 된다.

  1. graphql에서 에러를 onError로 캐치한다.
    if 에러가 unauthenticated면 ...
  2. restoreAccessToken을 요청
  3. 방금 실패한 API 요청 재시도

유저 입장에서는 버튼 한 번 클릭했는데 뒤에서 수많은 과정이 일어나는 것이다.
이것을 silentAuth라고 부른다.

백엔드에서 하나의 폴더를 두개로 만들고 API들을 나눠담는다.
폴더 하나를 서비스라고 부른다.

하나의 폴더를 인증 관련 폴더,
인증관련 API들이 있는 백엔드 => AuthService

인가 관련 폴더
리소스(자원)들이 있는 백엔드 => ResourceService

만약, 인증 관련된 API를 제공해주는 회사가 있다면 ?
Open Authentication => OAuth 인증을 오픈해줬다.
흔히 소셜로그인이라고 한다. (ex 카카오톡, 구글...)
AuthService를 제공해주고 인가만 만들어서 사용할 수 있는 방식이다.

기능에 따라 폴더를 나누는 것, MicroService-MSA

왜 나눌까?

합쳐져 있을 때 yarn dev가 실행이 되고, 소스코드에 문제가 있으면 전체가 실행이 안되어버리는 상황이 된다.

하지만 나누어서 관리를 한다면 게시판 소스코드에 문제가 생겼을 때 게시판만 작동을 하지 않고 다른 기능들은 작동을 한다.

배포를 할 때는 yarn build를 먼저 실행하는데, 모든 서비스를 압축하고 최적화하는데 비효율적이다.
만약 게시판에 코드에 문제가 생겨서 다시 배포해야하는 경우에 게시판만 다시 배포할 수 있다.

< 실습>
yarn add graphql-request

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

  // ////////////////////////////////////////////////////////////

  const errorLink = onError(({ graphQLErrors, operation, forward }) => {
    // 1-1. 에러를 캐치
    if (graphQLErrors) {
      for (const err of graphQLErrors) {
        // 1-2. 해당 에러가 토큰 만료 에러인지 체크(UNAUTHENTICATED)
        if (err.extensions.code === "UNAUTHENTICATED") {
          // 2-1. refreshToken으로 accessToken을 재발급 받기
          getAccessToken().then((newAccessToken) => {
            // 2-2. 재발급 받은 AccessToken 저장하기
            setAccessToken(newAccessToken);

            // 3-1. 재발급 받은 accessToken으로 방금 실패한 쿼리 재요청하기
            //  operation.getContext().headers; // 이 쿼리에서 작성됐던 헤더를 가지고 온다
            operation.setContext({
              headers: {
                ...operation.getContext().headers, // 기존 헤더는 그대로 가져오기
                Authorization: `Bearer ${newAccessToken}`, //  accessToken만 바꿔치기
              },
            });

            // 3-2. 변경된 operation 재요청하기
            return forward(operation);
          });
        }
      }
    }
  });

  const uploadLink = createUploadLink({
    uri: "https://backend06.codebootcamp.co.kr/graphql",
    headers: { Authorization: `Bearer ${accessToken}` },
    credentials: "include",
  });
  const client = new ApolloClient({
    link: ApolloLink.from([errorLink, uploadLink]),
    cache: new InMemoryCache(),
  });

아폴로 세팅

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

const RESTORE_ACCESS_TOKEN = gql`
  mutation restoreAccessToken {
    restoreAccessToken {
      accessToken
    }
  }
`;

export const getAccessToken = async () => {
  try {
    const graphQLClient = new GraphQLClient(
      "https://backend06.codebootcamp.co.kr/graphql", // secure 옵션 때문에 https로 변경
      { credentials: "include" }
    );

    const result = await graphQLClient.request(RESTORE_ACCESS_TOKEN);
    const newAccessToken = result.restoreAccessToken.accessToken;
    return newAccessToken;
  } catch (error) {
    console.log(error);
  }
};

graphql-request를 라이브러리로 따로 빼주기

grapql-request를 쓰는 이유?

onError가 세팅이 되면 errorLink로 첨부가 된다.
아폴로세팅이 완료되어야 useMutation이 사용가능한데 errorLink를 만드는 과정에서는 사용불가능하다.
그래서 쿼리를 백엔드로 보내주려면 axios나 GraphqlClient 둘 중의 하나 방법으로 가능하다.

// graphql client를 사용하지 않고 axios를 사용해 백엔드로 요청하는 방법
axios.post("https://backend06.codebootcamp.co.kr/graphql", {
  query: `
  mutation restoreAccessToken {
    restoreAccessToken {
      accessToken
    }
  }
`
})

Graphql

graphql이 사실은 Rest-API?

=> 특정 주소의 객체를 등록해줘

엔드포인트는 하나, 백에서 함수를 여러개 만들자.
그리고 실행시키고 싶은 함수를 보내자.

rest-api의 post방식으로 엔드 포인트를 하나로 합쳐서 (graphql이라는 걸로) 뒤에서 함수를 만들고 프론트에서 요청을 한다.
나는 네트워크에 fetch를 했는데 왜 post가 뜨지 ? => graphql은 endpoint가 하나인 post 방식의 rest-api이기 때문에

요청이 실패해도 200이 뜨는 이유 ?

3개, 4개를 묶어서 보내기 때문에(한 번에 여러개를 보내기 때문에) 모두가 성공할지, 모두가 실패할지, 뭐가 성공하고 뭐가 실패할지 모르기 때문에 요청 자체는 무조건 성공이다.

오버페칭 : 나는 writer만 받아오고 싶은데 다 받아올 수 밖에 없음 ( 오버해서 받아온다 ) => rest-api는 어쩔 수 없음
graphql은 오버페칭 문제를 해결할 수 있다!

언더페칭 : 게시글 등록을 하면서 유저 정보도 불러오고 싶은데.. 한개씩 밖에 못함 ( rest-api )
어차피 한 번 날아가는 요청에 여러개를 묶어 전달해주게 되므로 graphql이 해결
네트워크 비용을 획기적으로 줄여준다!

맨 앞 query는 mutation이든 query든 무조건 query
=> graphql을 만든 사람들이 그렇게 정해두었음

axios로도, graphql로도 날릴 수 있다.

profile
알고 쓰자!

0개의 댓글