30일) 유저들은 모를거야.. 로그인하고 토큰이 만료되면, refreshToken으로 쥐도새도 모르게 새로운 accessToken을 바꿔치기 한다는것을 ..! / JWT/UNAUTHENTED error/ CODE CAMP FE 6기

김아름·2022년 4월 25일
1

코드캠프6기

목록 보기
30/36
post-thumbnail
  • 7주차의 커리큘럼 ! (꺄 드뎌 refreshToken 배워서 토큰만료 에러를 보지 않을수 있게 되었다!)
  • 오늘의 알고리즘 문제 :
    https://programmers.co.kr/learn/courses/30/lessons/64061
    크레인 인형뽑기 문제..!
    점점 알고리즘 문제가 손을 대기도 어려울 만큼 난이도가 올라간다..
    포기하지 말고 꼭 풀이 복습하고, 자바스크립트 문제부터 차근히 풀어가자!
    -세준 멘토님의 정석 풀이와 foreach풀이
  • 오늘 배울 내용
    오늘은 지금까지 일정 시간이 지나면 만료되는 accessToken을 해결해줄 RefreshToken을 알아봤죠!
    _이를 위해 로그인 프로세스를 한번 더 알아봤습니다!


    Browser에서 로그인 확인 필요한 데이터 요청을 Backend로 보낼 경우, 예전에는 백엔드의 메모리세션에 로그인 정보들을 기록했다고 했죠!
    하지만 이 방식은 메모리가 다 차게 될 경우 더 이상 정보 저장을 할 수 없다는 단점이 있었습니다! 이를 해결 하기 위해 같은 정보를 가진 백엔드 서버를 여러개 만들어 처리했다 했죠!
    그렇지만 정보는 같지만 처리는 다르기 때문에 특정 로그인 기록이 남아있는 백엔드 서버가 아니라면 특정 유저의 로그인 유무를 인지하지 못한다고 했습니다!


    위 대안으로 로그인 정보를 DB에 넣는 방식을 사용하게 되었죠! 하지만 이 방식도 문제점을 전부 해결하진 못했습니다! 데이터베이스에서 병목현상이 일어나게 되었으니까요!
    그렇다고 백엔드 서버와같이 DB를 여러개 복사하기는 쉽지 않았습니다!


    그래서 나온 방법이 인증 테이블 파티셔닝 이였는데, 정보를 나눠 보관하는 방식을 사용했죠! (Ex. 1번부터 3000번까지는 1번 DB에 저장…)
    위의 문제점들을 해결한 방식이였지만, 위 방식도 결국 DB에 접근해서 데이터를 꺼내와야하는 번거로움이 있었습니다.


    앞서 알아본 흐름들과 대안들의 최종점으로 JWT가 나온 것이였습니다! JWT는 토큰 자체에 로그인 정보가 들어있었기에 백엔드에서 DB로 갈 필요가 없어졌죠!
    하지만 JWT도 백엔드에 들어가 접속의 유무만 구분하기에 중간에 탈취가 되어도 이 정보가 맞는지 DB검증이 불가하다는 한계가 있었습니다.
    그렇기에 만료시간을 주어 시간이 지나면 재발급(refreshToken)시키게 된 것이였죠!


    전체적인 흐름도 알아봤습니다!
    로그인을 진행했을때 2개의 결과물을 보내주었죠! accessToken과 refreshToken이였습니다!
    이렇게 받아온 accessToken을 state에 담아주고 refreshToken은 cookie에 담아주었죠!
    로그인 유무가 필요한 특정 API를 요청한다고 가정해봅니다, accessToken을 함께 넘겨 주어 인가를 받게 되겠죠!? accessToken이 만료되지않았다면 잘 작동 할겁니다.


    하지만 토큰이 만료가 된 경우는 어떻게 되었나요!? 만료된 토큰을 가지고 요청을 보냈을 경우에는 Browser에게 UNAUTHENTED 에러를 반환한다고 했습니다!
    이 에러를 받게 되면 Browser단에서 app.tsx부분에서 받은 에러가 어떤 에러인지 체크한 후 인가 관련 에러일 경우 refreshToken을 가지고 accessToken 재발급 요청을 하게 되었죠!
    그렇게 새로 발급받은 accessToken을 state에 다시 담아준 후 기존에 요청에 실패했던 API를 새 accessToken을 가지고 재요청하는 것이였습니다!
    이 전체적인 흐름의 이해가 중요했습니다!


    실습을 통해 app.tsx에 몇 가지 설정을 해주었죠!
    uploadLink부분을 좀 수정해주었습니다. 우리는 데이터 암호화를 사용하기위해 기존의 http uri를 https로 변경해주었고, 쿠키를 포함시켜 보내기 위해 credentials를 include로 변경시켜주었습니다!
    또한, onError()를 사용해 errorLink를 만들어 주었죠! 이 onError()안에는 콜백함수가 들어갔습니다!
    콜백함수의 인자로는 뭐가 들어갔는지 기억나시나요!? graphQLErrors operation, forward 였죠!
    이 안에는
  1. 만약 gql에러가 있다면
  2. 에러를 하나씩 뽑아 해당 에러가 토큰 만료 에러(UNAUTHENTICATED)인지 확인해주고
  3. 토큰 만료 에러라면 refreshToken으로 accessToken을 재발급 시켜주어야 하는데, 이 부분에서 ApolloClient세팅이 끝나지 않은 시점이기에 restoreAccessToken(useMutation)요청이 불가능했죠! 그렇기 때문에 우리는 graphql-request라이브러리를 사용하여 요청했습니다!
  4. 재발급 받은 accessToken을 setAccessToken()을 통해 저장시켜주고,
  5. 방금 실패한 쿼리의 정보가 담긴 operation의 설정을 operation.setContext({})를 활용해, accessToken만 변경하여 forword(operation)로 재요청
    의 내용이 들어갔고, 위 에러링크를 client(ApolloClient)에 연결 시켜주었죠!
    useEffect에서 localStorage에 저장시키던 부분도 getAccessToken().then()를 통해 손쉽게 해결할 수 있게 되었습니다


    postMan으로 gql요청도 보내봤습니다!
    이로써 우리가 알게된 것이 있었죠! gql은 항상 POST 방식이고 endPoint가 하나인 restAPI 였었죠!
    또한 POST 과정에서 데이터를 넣어 보내, underFetching문제를 해결했던 것이였습니다!
    중요했던 포인트는 각각의 요청에는 에러코드를 따로 줄 수 있었지만, POST 전체에 대한 요청은 항상 200(성공)코드를 받게 된다는 점이였습니다!_

- 드디어 등장한 RefreshToken

  • 우리는 여지껏 토큰 만료가 되는 accesstoken을 썼었다
    (막간의 토큰 저장방식 복습)
    (로그인해서 토큰 받아와서 로컬스토리지에 저장해주는 방식으로!)

    -백엔드 컴퓨터에 메모리세션에 로그인유저정보 저장한다고 했지!
    (그 상태는 stateful이라 한다, 그렇게 되어버리면
    여러대의 컴퓨터로 유저정보를 나누어 저장할 수 없어서,
    stateless상태로 만들어서, 정보는 db에 저장하게 되었따)


    -백엔드 컴퓨터를 추가해서 나누는것이 scaleout, scaleup(?)
    -데이터베이스를 나누기 시작했다 - 파티셔닝(수직,수평(샤딩))


    -데이터베이스 저장방식
    • 메모리(변수등 날아감), 디스크(유지)
      (우리가 사용하는 1번 로그인 유저정보 저장방식)
    • 메모리 기반 데이터 베이스가 빨라 ! 그래서 db앞에다가
      redis라는 메모리 기반 db를 두고 사용한다.
      (2번방식)
    • 로그인 유저 정보를 객체로 만들어서, 토큰처럼 만들어서(JWT):
      제이슨웹토큰 (객체처럼 된 웹토큰이다), 얘를 복호화 했을때 객체 정보가 나오도록 !
      (그러면 db에 유저정보를 저장할 필요가 없다 )


      우리는 그래서 localstorage에 저장했었는데, 위험한거 알지!
      state에 accesstoken을 저장해볼예정이다 !

  • 이메일과 비번을 쳐서 토큰을 받는 과정 : 인증(authentication)
  • 뮤테이션 요청이 왔을때, accesstoken을 확인하고 응답을 주는 과정 : 인가(authorization)

  • JWT를 두개 만든다고?
    하나는 accesstoken이라해서 보내고, 하나는 refreshtoken으로 보낸다
    (result.data.login~ 여기서 받았던 부분을 페이로드라고 한다 )
    accesstoken은 페이로드로 받는다
    refreshroken은 쿠키로 받는다 바삭!

    cookie에는 httpOnly라는 옵션이 있다(자바스크립트로 건드릴수 없다, result.~ 이렇게 못받는다)
  • 뿐만아니라 secure 옵션을 줄 수 있다 (얘는 https로만 통신하도록 한다)

    처음 로그인 하면 브라우저의 state(accesstoken) cookie(refreshToken) 이 저장된다
    시간이 지난후 accesstoken이 만료된 후 다른 뮤테이션 보낼때,
    '인가' 과정을 거치는데 , 그때 뜨는것이 토큰만료!
    -> error이름은 UNAUTHENTICATED라고 뜬다 !


    브라우저 아폴로 설정에 추가를 하자, 저 오류가 뜨면 restore accesstoken 요청을 하자
    (refresh token을 넣어줘야한다, 쿠키에 저장된건 자동적으로 백엔드에 들어가니까 ㄱㅊ)
  • refresh token이 만료가 되었는지 백엔드에서 확인 ,
    그것도 안되면 -> 로그인 페이지로 이동
    refresh token은 기간이 남아있다 -> 새로운 JWT토큰을 하나 더 만들어서 돌려준다

  • UNAUTHENTICATED 떠서 실패했던 api요청을 다시 보낸다
    수많은 error를 잘 잡을수 있어야 한다
  • onerror에서 unauthenticated에러이면, refreshtoken으로
    restoreaccesstoken을 요청하면 된다. 그리고 실패한 api요청을 재시도 한다.
    (토큰을 조용히 재발급 받아서 다시 로그인한것과 같다-slientAuth)
  • 저 login, logout,... 토큰과 로그인 관련된 api를 뭉터리, Createboards 뭉터리 지어서 다른 컴터에 나누어 저장했다고 해보자,

  • 토큰과 로그인 관련된 api 상자부분 : AuthService라고 한다

  • Createboards 상자부분 : Resourceservice라고 한다
    -> 하나의 폴더를 두개로(인증과 인가) 나누어서 백엔드 컴퓨터에 나누어 담은것이다

    -> 너네는 인가폴더만 처리해!
    백엔드 오픈api형태로 해서 인증부분을 제공해줄게!등장
    -> Open Authentication (OAuth)라고 한다 /
    소셜로그인 (구글로그인, 카카오 로그인..)이런거 !!

  • 로그인, 게시물, 상품을 각각 다른 폴더에 요청..!
  • 관련된 데이터를 담아주는 db로 분산시켜줌
  • 마이크로서비스 - MSA로 잘게 쪼개게 된다
  • 나눠서 관리하게 되면 유지보수 관리에도 편하다, 베포할때도 폴더별로 편하게 안되는곳만 안되게
  • 실습해보자!
  1. 로그인해보자
  2. 받아온 토큰이 만료가 되면
  3. refresh토큰이 다시 발급 잘해주는지 (세팅은 아폴로에서 하는것이다 graphql api는 아폴로를 다 거쳐서 지나가는거니까 ! )
  4. network통해서 봐보자


    아폴로 세팅에 들어가서 토큰만료 error부터 지정해주고 있는 모습이다

    2번 부분에 해당하는 refreshtoken으로 재발급 받으려면 백엔드 api의 restoreAccessToken 뮤테이션을 보내야한다
    근데, 2번부분에서는 usemutation을 할수 없다. 근데 아폴로 없이도 뮤테이션 요청이 가능하다 axios로!
    (rest-api처럼)postman으로 날려도 되는데, 쉽게 요청가능한 graphql 라이브러리를 써보자
    https://www.npmjs.com/package/graphql-request

    우리가 작성해준게 저 부분이래ㅔ!
    .then해서 받는걸 보니까 promise를 반환하겠네,
    그러면 async await로 써도 될것이고 !

    독스에서 제일 비슷한 저 부분을 따라해보자 !

    지금은 apollo에서 usemutation 못하니까 다른 방식으로 하려는거 기억하고,
    yarn add graphql-request 해주고
    import {GraphQLClient,gql} from 'graphql-request' 여기껄 써보자 !
  const RESTORE_ACCESS_TOKEN =gql`
        mutation restoreAccessToken{
            restoreAccessToken{
                accessToken
            }
        }
    `
// 아폴로 독스에 나와있어요 
    // graphQLErrors 는 구조분해 할당으로 받았고, 방금 실패했던 쿼리는 operation, 걔를 전송해줘 하면 forward 쓰면 된다 
    const errorLink = onError(({graphQLErrors, operation,forward})=>{
        // 1. error를 캐치
        if(graphQLErrors){
            // 독스에 나와있는 내용 그대로이다 그 안에있는 에러 개수만큼 한개씩 반복문 돌린다
            for(const err of graphQLErrors){
                // 1- 1. 해당 에러가 토큰만료 에러인지 체크(UNAUTHENTICATED)
                if(err.extensions.code === "UNAUTHENTICATED"){
                    // 2-1.Refresh토큰으로 accesstoken을 재발급 받기 
                    const graphQLClient = new GraphQLClient("https://backend06.codebootcamp.co.kr/graphql");

                    const result = await graphQLClient.request(RESTORE_ACCESS_TOKEN)
                    // 독스있는  그대로 쿼리 담아주고 결과를 담아보자 
                    // result안에 새로 받은 accesstoken이 들어오겠지
                   const newAccessToken = result.restoreAccessToken.accessToken
                //    2-2.새로받은 토큰을 꺼내서 글로벌스테이트에 저장해주자 
                    setAccessToken(newAccessToken)
                    // 3.재발급 받은 accesstoken으로 방금 실패한 쿼리 재 요청하기 
                    // operation 쿼리에서 모든것 다 납두고 헤더만 바꿔치기 (새로받은 토큰을 넣어줘야 하니까)
                    // operation.getContext().headers 헤더를 가져오자 말고 헤더를 새롭게 만들어주자 
                    operation.setContext({
                        headers: {
                            ...operation.getContext().headers,
                            // 3. 원래있던 헤더의 내용을 가져오고
                            // 1. 헤더 안에는 현재 authorization밖에 없어서 걔를 통째로 갈아 엎어도 된다
                            Authorization:`Bearer ${newAccessToken}` 
                            // 2. 주의 사항 : 다른 여러가지들이 들어있으면 사라지게 되니까 덮어쓰기는 위험하다! 
                            // 4. accessToken만 바꿔주자! 
                        }
                    }) 
                    return forward(operation)
                    // 변경된 operation 재요청하기 
                    

                }
            }
        }

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

여기서 주의할 점은, refreshtoken은 주소를 https로 바꾸어줘야 한다 !
^GraphQLClient을 사용해서 뮤테이션을 보내보는 코드이당
완성한 후
두개를 컴포넌트로 빼놓자 왜냐면 여러군데에서 이 토큰 바꿔치기가 쓰일수 있자나 !
그리고 들어가서 확인해보자 (5초토큰만료되는예로)
refresh토큰이 http헤더에 들어온모습 ( 쿠키에 담아준다!
근데 왜 어플리케이션 쿠키에 가면 없어?
-> 그걸 그래서 코드에 추가해조야대

const uploadLink = createUploadLink({
        uri: "http://backend06.codebootcamp.co.kr/graphql",
        headers: {Authorization:`Bearer ${accessToken}` },
        credentials:"include",
      })
 const graphQLClient = new GraphQLClient("https://backend06.codebootcamp.co.kr/graphql",
        {credentials: "include"});

이렇게 추가 !
쿠키에 refreshToken을 추가해준다는 의미!!

기존의 header를 불러오고 request Headers의 authorization만 바꿔준다는거 !!

- Graphql이 rest-api라고?

  • restapi쓸때 get 방식과 post방식을 썼었다.

    이런식으로 !

    rest-api를 사용하다가
    api는 하나를 만들어놓고(post/graphql)(createBoard,createproduct,,,,),
    백엔드에서 함수를 여러개 만들어놓으면 어떨까?
    데이터를 기존의 방식으로 보내지말고,
    함수를 보내자((){}그 안에 뮤테이션 쿼리 쓰듯이 써서 보내자 )
    그러면 백엔드에서는 일치하는 함수명에 해당하는 함수를 실행해서 안에 값을 넣어주자

-> 이렇게 restapi가 발전하여 grapghql이 된것이다 !


그래프큐엘은 엔드포인트가 하나인 restapi인것이다.
실행할 함수를 담아서 보내줘야하니까 항상 포스트 방식이다
각각에 대해선 성공실패가 뜨지만
실패를 해도 항상 200이라고 뜨는 이유는 한번에 여러개 api를 실행하는데,
누구는 성공하고 누구는 실패하는데 무조건 성공이라고 밷어낸다
-> rest-api의 오버패칭문제를 해결할 수 있다
-> axios요청을 두번해야하는 언더패칭문제 (내가 원하는건 2갠데 한개씩밖에 못하는 restapi..)


grapghql에서는 항상 POST를 볼 수 있다.
포스트맨에서 된다는 얘기는 axios에ㅓ도 된다는 얘기
Rest-api할때 했던 방법

그래프큐엘도 엔드포인트가 하나인 restapi이다! 그러니까 이렇게 포스트맨에서도 되고, axios도 된다는 얘기다

오늘 getaccesstoken을 grapgqlclient 말고 axios쓰는 법

profile
SUNNY SUMMER ! 같이 일하고 싶은 개발자 여름이의 초심을 잃지 않기 위한 주절주절 부트캠프 시절 블로그.

2개의 댓글

comment-user-thumbnail
2022년 4월 25일

전에 토큰들과 치고박고 싸웠던 적이 있어서,, 글에 손이 갔는데
프론트쪽에선 어떻게 처리하는지 덕분에 잘 읽고 갑니다 😁
(뒷단에서도 어떻게 굴러가는지 공부하신것 같아서 멋지십니다!!!! 👍👍)

1개의 답글