refreshToken

Kimu·2021년 10월 21일
0

내 머릿속의 지우개...처럼 현재 우리가 사용하는 테스트 서버에서는 한 시간짜리 accessToken을 발급해주는 시스템이라, 한시간마다 액세트토큰을 지우고 새로 발급받아야 권한없음 에러가 발생하지 않는다. 현업에서도 한시간까지는 아니지만 6시간 최대 24시간 정도로 액세스토큰 만료시간이 있다. 왜냐하면 accessToken은 털릴 수 있고, 털리면 해커는 나인척 게시판을 돌아다닐 수 있기 때문에 털린 액세스토큰을 자꾸 만료시켜버리는 것이다.

그렇다면 이런 상황에서 사용자가 피로감을 느끼지 않고 또한 보안을 유지하면서 로그인 상태를 유지하려면 어떻게 하면 좋을까 라는 문제가 생긴다.

그것을 해결하는 것이 바로 리프레시 토큰을 발급받는 것이다. 리프레시 토큰은 로컬이나 세션스토리지가 아닌 쿠키에 저장한다. 이 토큰은 브라우저 밖으로 꺼내 사용할 수 없는 "html only" type이다. 우리는 이것을 javascript로 가져올 수 없기 때문에 이걸 가지고 뭘 할 수는 없다. 하지만 브라우저에서는 조회할 수 있다. 그리고 쿠키에 저장된 것은 http통신에서 우리가 따로 작업을 하지 않아도 헤더에 첨부된다.

따라서 우리의 전략은, 로그인을 하면 리프레시 토큰을 받아 쿠키에 저장하고, 토큰이 만료되어 우리가 보낸 쿼리가 권한만료의 에러를 만났을 경우에, 리프레시 토큰이 있는지 확인해서, 있으면 액세스토큰을 재발급받고, 방금 실패한 쿼리를 다시 날려서 요청한 응답을 성공시키는 코딩을 하려고 한다.

자, 차근차근 해보자.

  1. src > components > commons > libaraies 에 getAccessToken.ts 파일을 만든다.
    여기서 yarn add graphql-request로 graphql-request를 다운받는데 이 이유는 이 파일을 사용할 _app.js에서 아직 그래프큐엘 쿼리를 요청할 수 있는 상태가 아닐 때에 쿼리문을 작성해야하기 때문이다. 뭔가 닭이 먼저냐 달걀이 먼저냐 또는 이 라이브러리가 있는데 그럼 왜 태그를 아폴로 클라이언트로 감싸냐 할 수 있는데 일단 시키는대로 할 뿐이다.ㅎㅎ
  1. _app.tsx 에서
    import { getAccessToken } from "../src/components/commons/libraries/getAccessToken";
    import { onError } from "@apollo/client/link/error"
 를 한다. 아까 라이브러리에 만든 함수 가져오고, 아폴로 클라이언트 링크 에러에서 온에러라는 녀석을 가져온다
```js
 useEffect(() => {
    // refreshToken적용하면 아래 네 줄 필요 없음
    // const accessToken = localStorage.getItem("accessToken") || ""
    // const userInfo = localStorage.getItem("userInfo") || {}
    // setAccessToken(accessToken)
    // setUserInfo(userInfo)
    if (localStorage.getItem("refreshToken")) getAccessToken(setAccessToken)
  }, [])

이렇게 수정하여 둔다. 이 코드는 로그인 페이지에서 로그인하면 로컬스토리지에 "refreshToken"이라는 변수를 생성하여 그 값을 true로 바꿔주는 코드와 함께 읽어야한다. 아까 말했듯이 리프레시토큰은 조회도 안되고 자바스크립트에서 가져와서 사용할 수 없다. 그래서 로그인할 때 서버에서 리프레시 토큰을 주니까 리프레시 토큰이 있을 것이고, 그 상태를 로컬스토리지에 리프레시토큰이라는 변수를 만들어 상태를 트루로 주면 리프레시토큰이 있는 상태를 알려주는 것이다.
아까 아폴로에서 가져온 온에러를 사용할 때이다,

 const errorLink = onError(
    ({graphQLErrors, operation, forward}) => {
      if (graphQLErrors) {
        for (const err of graphQLErrors) {
          if (err.extensions?.code === "UNAUTHENTICATED") {
            operation.setContext({ // 실행하려다가 토큰만료로 실패한 쿼리
              headers: {
                ...operation.getContext().headers, // 기존의 헤더 정보를 가져옴
                authorization: `Bearer ${getAccessToken(setAccessToken)}`
              }
            })
            return forward(operation) // 다시해줄 작업 (아까 그 쿼리 다시 날리기)
          }
        }
      }
    }
  )

onError의 인자는 순서대로, 그래프큐엘이 주는 에러가 무엇인지, 실행하려다가 방금 에러가 나서 실패한 작업이 무엇인지, 앞으로 할 일이 무엇인지를 매개변수로 받는다.
토큰이 만료되었음을 알려주는 에러코드인 "UNAUTHENTICATED"가 에러인 경우라면, operation의 헤더정보를 불러와서, forward에 그 오퍼레이션을 넣어 실행시켜준다는 코드다.
이게 끝이 아니다.

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

기존의 작업한 업로드링크 코드에서 마지막줄 크리덴셜을 인클루드로 바꿔주고, 클라이언트의 새로운 생성 링크에서 에러링크도 배열 안에 적어준다.

이렇게하면 _app.js작업이 끝났다.

이제 사용하려는 로그인페이지와 권한분기 페이지가 가서 지금의 논리대로 약간의 변경을 해주자.

  1. 나는 메인페이지의 로그인과 헤더에 로그아웃 컴포넌트에서 로그아웃함수를 약간 수정해줘야한다. 글로벌 컨텍스트에다가 받아온 액세트토큰을 적어주는 코드를 주석처리했는데, 그러면 받아온 액세트토큰을 사용하지 못해 에러가 난다. 다시 주석해제하고 글로벌 컨텍스트를 임포트해서 setAccessToken을 불러와서 값을 저장해주자.
 const { setAccessToken } = useContext(GlobalContext)
async function onClickLogin() {
    const result = await loginUser({variables: {...userInput}})
    // localStorage.setItem("accessToken", result.data?.loginUser.accessToken)
 setAccessToken(result.data?.loginUser.accessToken)
    localStorage.setItem("refreshToken", "true")
    router.push('/mypage')
    console.log("10/21", result.data?.loginUser.accessToken)
  }

로그인 시에 기존의 방법을 지우고, 로컬스토리지에 리프레시토큰이라는 변수에 트루값을 주어 생성한다

const onClickLogout = () => {
    // logoutUser()
    localStorage.removeItem("refreshToken")
    setUserInfo({})
    setAccessToken('')
    Modal.confirm({ content: "로그아웃 성공"})
    router.push('/login')
  }

로그아웃하면 로컬스토리지의 리프레시토큰을 삭제해버린다.
위치가 왜 이런지 잘 모르겠지만 권한분기파일은 src> components > commons > hocs > withAuth.tsx로 있다.
여기서도

useEffect(() => {
    if (!localStorage.getItem("refreshToken")) {
      alert("로그인이 필요합니다")
      router.push("/login")
    }
  }, [])

로컬스토리지에 리프레시 토큰이 없다면 으로 바꿔준다.

방금 리프레시 토큰을 쿠키에서 조회할 수 없는 문제가 있었는데, 이것을 해결하는 방법은 요청하는 백엔드 주소에 http를 https로 바꿔주어야 한다. 바꾸니까 바로 들어오네.

이게 전부다. 참 쉽죠? 🦊

profile
지속가능한 개발자

0개의 댓글