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을 새로 발급받는 과정
- AccessToken 만료 후 인가 요청
- 해당 오류를 포착해서 인가 에러인지 체크
- RefreshToken으로 AccessToken 재발급 요청
- 발급 받은 AccessToken을 state에 재저장
- 방금 실패했던(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이다.