Code.camp/Codecamp

본캠프 30일차

Jayy 2022. 12. 9. 13:35

RefreshToken

RefreshToken 이란?

**AccessToken** 은 사용자의 로그인 정보를 담고 있는 **JWT토큰 데이터**이다.

AccessToken 데이터는 일정 시간 동안만 사용할 수 있도록 만료 기한이 정해져 있다.

만료 기한이 지나고 사용자가 로그인 정보가 필요한 페이지에 접근하려고 하면

백엔드에서 미리 지정해둔 경로로 redirect 되거나 에러가 뜨게 된다.

그렇기 때문에 **AccessToken의 만료 기한이 지나면 새로운 AccessToken을 받아와야 한다.**

이러한 과정에 사용되는 토큰을 RefreshToken 이라고 한다.

예를 들어보면,

사용자가 올바른 이메일과 비밀번호를 입력해서 로그인하면

백엔드에서 AccessToken과 RefreshToken을 받아오게 된다.

이 때 **AccessToken**은 1~2시간 정도의 짧은 만료 기한을 가지고 있고,

**RefreshToken**은 2주~1개월 정도의 긴 만료 기한을 가지고 있다.

그렇다면 이 RefreshToken을 어디에 저장하는 것인가?

바로 쿠키에 RefreshToken을 담아서 받아오게 된다.

쿠키의 secure / httpOnly 옵션

쿠키라고 해서 매우 안전한 것은 아니지만 로컬/세션 스토리지와는 다르게 secure, httpOnly 등의 옵션을 설정할 수 다.

**httpOnly** : 브라우저에서 Javascript를 이용해 쿠키에 접근할 수 없고, 통신으로만 해당 데이터를 주고받을 수 있다.

**secure** : https 통신 시에만 해당 쿠키를 받아올 수 있다.

RefreshToken을 이용해 AccessToken을 새로 발급받는 과정

  1. AccessToken 만료 후 인가 요청
  2. 해당 오류를 포착해서 인가 에러인지 체크
  3. RefreshToken으로 AccessToken 재발급 요청
  4. 발급 받은 AccessToken을 state에 재저장
  5. 방금 실패했던(error) API를 재요청

소셜 로그인 ( OperAuth 서비스 ) 위와 같은 로그인 서비스를 카카오에서 제공을 하고, 네이버에서 제공을 하고, 구글에서 제공을 한다.  이런 서비스들을 OAuth 서비스라고 하며, 흔히 소셜 로그인이라고 부른다..

RefreshToken 실습

1. apollo 파일세팅 및 준비

리프레시 토큰을 받아오는 작업은 아폴로세팅을 해주는 부분에서 진행할 것 이다.

지금 부터는 Apollo-client에서 제공하는 **onError**라는 기능을 사용할 것이다.

// src/apollo/index.tsx 파일

export default function ApolloSetting(props: IApolloSettingProps) {
  const [accessToken, setAccessToken] = useRecoilState(accessTokenState);
  const [userInfo, setUserInfo] = useRecoilState(userInfoState);
	if (!accessToken || !userInfo) return;
	    setUserInfo(JSON.parse(userInfo));
	  }, []);
	
	const errorLink = onError(({ graphQLErrors, operation, forward })=>{
		// 1-1. 에러를 캐치

		// 1-2. 해당에러가 토큰만료 에러인지 체크(UNAUTHENTICATED)		

    // 2-1. refreshToken으로 accessToken을 재발급 받기
				
		// 2-2. 재발급 받은 accessToken 저장하기

		// 3-1. 재발급 받은 accessToken으로 방금 실패한 쿼리정보 수정하기

		// 3-2. 재발급 받은 accessToken으로 방금 수정한 쿼리 재요청하기

	})

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

	  return (
	    
	        {props.children}
	    
	  )
	}

uploadLink의 요청을 보내실 때 **uri 경로를 http에서 https로** 바꾸고, 민감한 정보 포함을 승인한다는 뜻의 **credentials: “include”** 옵션을 추가해주셔야 한다.

만일, **credentials: “include” 이 없다면 refreshToken을 쿠키에 못담을 뿐만아니라 쿠키에 담겨있는것들도 백엔드로 전송이 되지 않는다.**

graphQLErrors : 에러들을 캐치한다.
operation : 방금전에 실패했던 쿼리정보를 알아낸다.
forward : 실패했던 쿼리들을 재전송 한다.

2. errorLink 생성 및 구조 기반 설정

 

// src/apollo/index.tsx 파일의 errorLink부분

import { onError } from "@apollo/client/link/error";

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

refreshToken을 사용하기 위해서는 graphQL 요청을 보내야 하는데,

errorLink를 생성하는 코드는 ApolloProvider 바깥에 있기 때문에

useQuery나 useApolloClient등을 이용해 graphQL 요청을 보낼 수가 없다.

이러한 문제를 해결하기 위해서 **graphql-request**라는 라이브러리를 사용해 볼 것이다.

graphql-request 공식 문서를 참고해 다음과 같이

error를 캐치한 뒤 accessToken을 재발급 받는 코드를 적어준다.

// src/apollo/index.tsx 파일
import { GraphQLClient } from "graphql-request";

export default function ApolloSetting(props: IApolloSettingProps) {
  const [accessToken, setAccessToken] = useRecoilState(accessTokenState);
  const [userInfo, setUserInfo] = useRecoilState(userInfoState);
	const RESTORE_ACCESS_TOKEN = gql`
	  mutation restoreAccessToken {
	    restoreAccessToken {
	      accessToken
	    }
	  }
	`;
	
	if (!accessToken || !userInfo) return;
	    setUserInfo(JSON.parse(userInfo));
	  }, []);

// 리프레시 토큰 만료 에러 캐치 & 발급
		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을 재발급 받기
					const graphqlClient = new GraphQLClient(
          "<https://backend10.codebootcamp.co.kr/graphql>",
          { credentials: "include" }
	        );
					const result = await graphqlClient.request(RESTORE_ACCESS_TOKEN);
		      // RESTORE_ACCESS_TOKEM이라는 gql을 요청한 뒤 반환되는 결과값을 result에 담는다.
	        const newAccessToken = result.restoreAccessToken.accessToken;
	        // 2-2. 재발급 받은 accessToken 저장하기
	        setAccessToken(newAccessToken);

					//3-1. 재발급 받은 accessToken으로 방금 실패한 쿼리정보 수정하기
					if(typeof newAcessToken !== "string") return
					operation.setContext({
	                headers: {
	                  ...operation.getContext().headers,
	                  Authorization: `Bearer ${newAccessToken}`, // accessToken만 새걸로 바꿔치기
	                },
	              });
					//3-2. 재발급 받은 accessToken으로 방금 수정한 쿼리 재요청하기
					forward(operation)
        }
			}
		}
	})
	
	 const uploadLink = createUploadLink({
	    uri: "<https://backend10.codebootcamp.co.kr/graphql>",
	    headers: { Authorization: `Bearer ${accessToken}` },
		  credentials: "include",
	  });
	
	  const client = new ApolloClient({
	    link: ApolloLink.from([uploadLink]),
	    cache: APOLLO_CACHE,
	    connectToDevTools: true,
	  });
	
	  return (
	    
	        {props.children}
	    
	  )
	}

3. getAccessToken파일 분리

libraries 폴더로 이동해서 getAccessToken.ts 라는 이름의 파일을 신규 생성하고 graphql-request를 이용하여 accessToken을 재발급 받는 코드를 별도 함수로 분리하여 입력해 준다.

// getAccessToken.ts 내용

import { gql } from "@apollo/client";
import { GraphQLClient } from "graphql-request";

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

export async function getAccessToken() {
  try {
    const graphqlClient = new GraphQLClient(
      "<https://backend10.codebootcamp.co.kr/graphql>",
      {
        credentials: "include",
      }
    );
    const result = await graphqlClient.request(RESTORE_ACCESS_TOKEN);
    const newAccessToken = result.restoreAccessToken.accessToken;

    return newAccessToken;
  } catch (error) {
    console.log(error.message);
  }
}

이렇게 코드를 분리하고 나면, errorLink내부의 로직도 살짝 바뀌어야 한다.

분리한 함수를 import 해주시고, 함수의 return 값이 promise 이므로 .then을 이용해 이후의 코드를 작성할 수 있다.

// src/apollo/index.tsx 파일
import getAccessToken from '파일 경로'

export default function ApolloSetting(props: IApolloSettingProps) {
  const [accessToken, setAccessToken] = useRecoilState(accessTokenState);
  const [userInfo, setUserInfo] = useRecoilState(userInfoState);
	const RESTORE_ACCESS_TOKEN = gql`
	  mutation restoreAccessToken {
	    restoreAccessToken {
	      accessToken
	    }
	  }
	`;
	
	if (!accessToken || !userInfo) return;
	    setUserInfo(JSON.parse(userInfo));
	  }, []);

// 리프레시 토큰 만료 에러 캐치 & 발급
	const errorLink = onError(({ graphQLErrors, operation, forward }) => {
	    // 1-1. 에러를 캐치
	    if (graphQLErrors) {
	      console.log(graphQLErrors);
	      for (const err of graphQLErrors) {
	        // 1-2. 해당 에러가 토큰만료 에러인지 체크(UNAUTHENTICATED)
	        if (err.extensions.code === "UNAUTHENTICATED") {

	          // 2-1. refreshToken으로 accessToken을 재발급 받기
	          return fromPromise(
	            getAccessToken().then((newAccessToken) => {
	              // 2-2. 재발급 받은 accessToken 저장하기
	              setAccessToken(newAccessToken);
	
	              // 3-1. 재발급 받은 accessToken으로 방금 실패한 쿼리 재요청하기
	              operation.setContext({
	                headers: {
	                  ...operation.getContext().headers,
	                  Authorization: `Bearer ${newAccessToken}`, // accessToken만 새걸로 바꿔치기
	                },
	              });
	            })
	          ).flatMap(() => forward(operation)); // 3-2. 변경된 operation 재요청하기!!!
	        }
	      }
	    }
	  });
	
	 const uploadLink = createUploadLink({
	    uri: "<https://backend10.codebootcamp.co.kr/graphql>",
	    headers: { Authorization: `Bearer ${accessToken}` },
		  credentials: "include",
	  });
	
	  const client = new ApolloClient({
	    link: ApolloLink.from([uploadLink]),
	    cache: APOLLO_CACHE,
	    connectToDevTools: true,
	  });
	
	  return (
	    
	        {props.children}
	    
	  )
	}

 


 

Promise와 Observable

promise(프로미스)?

비동기 작업을 도와주는 도구

observable(옵저버블) 이란?

연속적인 비동기 작업 도와주는 도구.

  • 연속적인 비동기 작업이란?
    • 요청을 빠르게 여러번 보내는 것 

게시글 목록페이지에서 페이지 요청을 여러번 빠르게 했을경우, 백엔드에서 누른 순서대로 응답을 보내지 않는다.

예를 들어, 3번페이지를 요청했다가 빠르게 5번 페이지를 요청했을 경우 3번 페이지 요청을 취소 후 5번 페이지를 보내줘야 하는데 , 백엔드에서는 3번페이지를 보여주게 된다. 이런경우에는 3번 페이지 요청을 취소해야 한다. 그렇지 않으면 사용자의 불편한 경험을 초해 할 수 있기 때문이다.

하지만, 이런경우는 promise로 처리 하는게 쉽지 않다. 이럴 때 observable을 사용하게 된다.

apollo-client의 flatmap

알고리즘에서 보신 분도 있겠지만, 자바스크립트의 메소드와는 다른 flatMap이다.

여기서 사용하는 flatMap은 apollo-client에서 지원하는 flatMap이다.

 

'Code.camp > Codecamp' 카테고리의 다른 글

본캠프 32일차  (0) 2022.12.13
본캠프 31일차  (0) 2022.12.12
본캠프 29일차  (0) 2022.12.08
본캠프 28일차  (0) 2022.12.07
본캠프 27일차  (0) 2022.12.06