[30-1] RefreshToken
[30-2] 새로고침시 토큰 유지하는 방법
📂 AccessToken
AccessToken
: 사용자의 로그인 정보를 담고 있는 JWT토큰 데이터RefreshToken
이라고 한다.📂 RefreshToken
AccessToken
: 1~2시간 정도의 짧은 만료 기한RefreshToken
: 2주~1개월 정도의 긴 만료 기한쿠키
에 RefreshToken을 담아서 받아오게 된다.💡 쿠키의 secure / httpOnly 옵션
로컬 / 세션 스토리지와는 다르게 쿠키는 secure, httpOnly 등의 옵션을 설정할 수 있다.
httpOnly
: 브라우저에서 Javascript를 이용해 쿠키에 접근할 수 없고, 통신으로만 해당 데이터를 주고받을 수 있다.secure
: https 통신 시에만 해당 쿠키를 받아올 수 있다.
📂 AccessToken과 RefreshToken 두 가지 종류의 토큰을 받아오는 이유
📂 RefreshToken을 이용해 AccessToken을 새로 발급받는 과정
1️⃣ AccessToken 만료 후 인가 요청
2️⃣ 해당 오류를 포착해서 인가 에러인지 체크
3️⃣ RefreshToken으로 AccessToken 재발급 요청
4️⃣ 발급 받은 AccessToken을 state에 재저장
5️⃣ 방금 실패했던(error) API를 재요청
📂 마이크로 서비스 아키텍쳐 (Micro Service Architecture)
인증 서비스
: 일반적으로 인증/인가와 관련된 API리소스 서비스
: 컨텐츠를 처리하는 것과 관련된 API💡 MSA의 장점
- 각각의 서비스를 필요에 따라 다른 언어나 구조로 만들 수 있다.
- 백엔드 서비스가 다운되더라도 문제가 발생한 일부 마이크로 서비스만 다운될 뿐 서비스 전체가 접속 불가능해지는 사태는 일어나지 않는다.
🎯 MSA의 단점
- 서비스의 구조가 복잡하다.
📂 여러가지 쿼리 방식
useQuery
: 페이지에 접속하면 자동으로 바로 실행되어 data라는 변수에 fetch해온 데이터를 담아주며, 리렌더링 된다.useLazyQuery
: useQuery를 원하는 시점에 실행(버튼 클릭시)후 fetch해온 데이터를 data변수에 담아준다.useApolloClient
: 원하는 시점에 실행 후 fetch해온 데이터를 원하는 변수에 담을 수 있습니다. 따라서 axios 같은 느낌으로 사용이 가능하다.import { gql, useApolloClient, useLazyQuery, useQuery } from "@apollo/client"; import { IQuery } from "../../src/commons/types/generated/types"; const FETCH_USER_LOGGED_IN = gql` query fetchUserLoggedIn { fetchUserLoggedIn { email name } } `; export default function LoginSuccessPage() { // 1. 페이지 접속하면 자동으로 data에 받아지고, 리렌더링됨 // const { data } = useQuery<Pick<IQuery, "fetchUserLoggedIn">>(FETCH_USER_LOGGED_IN); // 2. 버튼 클릭시 직접 실행하면 data에 받아지고, 리렌더링됨 // const [myquery, { data }] = useLazyQuery(FETCH_USER_LOGGED_IN); // 3. axios와 동일 // const client = useApolloClient(); const client = useApolloClient(); const onClickButton = async () => { const result = await client.query({ query: FETCH_USER_LOGGED_IN, }); console.log(result); }; return ( <button onClick={onClickButton}>클릭하세요</button> ); }
📂 RefreshToken 적용
1️⃣ apollo 파일세팅
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: "graphql주소", headers: { Authorization: `Bearer ${accessToken}` }, credentials: "include", }); const client = new ApolloClient({ link: ApolloLink.from([uploadLink]), cache: APOLLO_CACHE, connectToDevTools: true, }); return ( <ApolloProvider client={client}> {props.children} </ApolloProvider> ) }
💡 graphQLErrors / operation / forward
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을 재발급 받기 } } } });
💡 graphql-requset 설치
yarn add graphql-request
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://backend-practice.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: "http://backend08.codebootcamp.co.kr/graphql", headers: { Authorization: `Bearer ${accessToken}` }, credentials: "include", }); const client = new ApolloClient({ link: ApolloLink.from([uploadLink]), cache: APOLLO_CACHE, connectToDevTools: true, }); return ( <ApolloProvider client={client}> {props.children} </ApolloProvider> ) }
3️⃣ getAccessToken파일 분리
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://backend-practice.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); } }
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: "http://backend08.codebootcamp.co.kr/graphql", headers: { Authorization: `Bearer ${accessToken}` }, credentials: "include", }); const client = new ApolloClient({ link: ApolloLink.from([uploadLink]), cache: APOLLO_CACHE, connectToDevTools: true, }); return ( <ApolloProvider client={client}> {props.children} </ApolloProvider> ) }
4️⃣ refreshToken 확인
// 3. 프리렌더링 무시 - useEffect 방법 // 토큰을 넣어두는 global state - recoilState const [accessToken, setAccessToken] = useRecoilState(accessTokenState); useEffect(() => { // 1. 기존방식(refreshToken 이전) // console.log("지금은 브라우저다!!!!!"); // const result = localStorage.getItem("accessToken"); // console.log(result); // if (result) setAccessToken(result); // 2. 새로운방식(refreshToken 이후) - 새로고침 이후에도 토큰 유지할 수 있도록 void getAccessToken().then((newAccessToken) => { setAccessToken(newAccessToken); }); }, []);