develop

refresh token 구현 - axios interseptors

crab. 2023. 1. 20. 16:15

다시 돌아가서

  • 인증방식에 대해 다시 생각해보자.
  • http 통신은 header에 요청에 대한 정보(인증 토큰)가 들어간다.
  • jwt방식은 서버가 클라이언트로 부터 받은 ID와 Password가 맞는지 확인하고 맞으면 JSON Web Token이라는 긴 문자열을 브라우저에 전송해준다.
    • JWT의 TOKEN을 만들기 위해서는 Header, Payload, Verify Signature 이렇게 세 가지가 필요하다.
    • Header : 이 세 가지 정보를 암호화 할 방식, 타입 등이 들어간다.
    • Payload : 서버에서 보낼 데이터가 들어간다. 일반적으로 유저의 고유 ID 값, 유효기간 등이 들어간다.
    • Verify Signature : Base64 방식으로 인코딩한 Header, Payload 그리고 SECRET KEY를 더한 후 서명된다.
    • 그래서 최종적으로는 : Encoded Header + "." + Encoded Payload + "." + Verify Signature가 된다.
  • 그럼 클라이언트는 이 JWT를 쿠키나 로컬스토리지에 저장하게 하고
  • 로그인 되어 있는 유저가 인증이 필요한 페이지를 클릭하면 자동으로 JWT를 헤더에 담아 서버로 보내게되고 서버는 이게 JWT가 있는지와 유통기한을 검사한 후 그에 맞는 필요한 정보를 전달해준다.
  • 다만, 이미 발급 된 JWT는 돌이킬 수 없다.
  • 그러니 악의적인 사용자는 유효기간 안에서 신나게 정보를 털어낼 수 있는 것이다.
  • 이를 방지하기 위해서 기존 Access Token 유효기간을 짧게 설정하고 Refresh Token이라는 새로운 토큰을 발급한다.
  • 그러면 Access Token을 탈취당해도 상대적으로 피해를 줄일 수 있을 것이다.

그럼 어떻게 refresh token을 구현할까?

  • 구현사항을 정의해보자.
    • 로그인 했을시 서버로부터 access, refresh token을 받고 localStorage에 저장해야한다.
    • 매 요청마다 헤더에 access token을 넣어 서버로 보내야 한다.
    • 또한 매 요청마다 이 access token의 만료기한이 지났는지 확인하고 지났다면
      • 서버와 약속된 api로 refresh 토큰을 보낸 다음
      • access token을 새롭게 받고
      • 받은 access을 localStorage에 저장하고
      • 헤더에 담아 서버로 보내 원래의 요청을 성공한다.
  • 좋다! 사실 글이 많아보여서 그렇지 어렵지 않다.
  • 항상 기억해두면 좋은건 결국 우리가 하는 것은 로켓발사알고리즘이 아니며
  • 어려운것은 익숙하지 않아서이다.

매 요청마다

  • api를 호출하다보면 그런 생각이 든다.(사실 나는 안들었었고.. 레퍼런스에 있길래 쓰면서 시작했다.. 필요해서 → 쓴다가 아닌, 처음부터 쓰니까 필요한지 몰랐다..)
    • 매번 axios 요청할때마다, 겹치는 부분을 기본 URL로 설정하고 싶다.
    • axios 사용할때마다 헤더를 매번 넣고 싶지 않다.
    • 에러가 발생했을때 공통으로 처리하고 싶다.
  • 이런 경우 interseptors를 사용하면 된다.

1) 정의

인터셉터는 1.요청하기 직전, 2. 응답을 받고 then, catch로 처리 직전에 가로챌 수 있다.

2) 구성

크게 3가지 부분으로 구성된다.

  • 인스턴스
  • request 설정
  • response 설정

그리고 request, response 설정은 각각 2개의 콜백함수를 받는다.

import axios from 'axios'

// axios 인스턴스를 생성합니다.
const instance = axios.create({
    baseURL: 'https://api.hnpwa.com',
    timeout: 1000
  });

/*
    1. 요청 인터셉터
    2개의 콜백 함수를 받습니다.
*/
instance.interceptors.request.use(
    function (config) {
        // 요청 성공 직전 호출됩니다.
        // axios 설정값을 넣습니다. (사용자 정의 설정도 추가 가능)
        return config;
    },
    function (error) {
        // 요청 에러 직전 호출됩니다.
        return Promise.reject(error);
    }
);

/*
    2. 응답 인터셉터
    2개의 콜백 함수를 받습니다.
*/
instance.interceptors.response.use(
    function (response) {
    /*
        http status가 200인 경우
        응답 성공 직전 호출됩니다.
        .then() 으로 이어집니다.
    */
        return response;
    },

    function (error) {
    /*
        http status가 200이 아닌 경우
        응답 에러 직전 호출됩니다.
        .catch() 으로 이어집니다.
    */
        return Promise.reject(error);
    }
);
  • 그렇다면 우리의 과정에서 이 interseptors를 어떻게 활용하면 좋을까?
  1. axios 인스턴스를 생성한다.
// axios 기본 주소 & header 타입 세팅
export const instance = axios.create({
  baseURL: process.env.REACT_APP_AUTH_SERVER,
  timeout: 5000,
});
  1. request를 설정한다.
//┏----------interceptor를 통한 header 설정----------┓
instance.interceptors.request.use(async config => {
  config.headers["Content-Type"] = "application/json; charset=utf-8";
  config.headers["X-Requested-With"] = "XMLHttpRequest";
  config.headers["Accept"] = "*/*";

  const accessToken = localStorage.getItem("accessToken");
  if (!accessToken) return config;

  config.headers.Authorization = `Bearer ${accessToken}`;
  return config;
});
  • 이렇게 하면 매 요청마다 자체적으로 로컬스토리지에 accessToken이 있는지 확인하고
  • 없으면 헤더를 그만 추가하고
  • 있으면 헤더에 accessToken을 넣어서
  • 모든 요청을 처리할 수 있다.
  • 편리하다!

로그인시 token을 받고 localStorage에 저장

  • 나는 redux-toolkit을 쓰고 있어서 createAsyncThunk를 썼지만
  • 그것을 쓰지 않는다고해도 두번째 코드의 if 부분만 보면 핵심은 와닿을것이다.(아니면 어쩔수 없다..)
  • 즉, 단순하게 서버로부터 받은 토큰 두가지를 localStorage.setItem을 이용해 로컬스토리지에 저장하면 된다.
    • (localStorage.setItem는 DOM인가 BOM인가, BOM은 DOM안에 속하는가? 옛날에 렌더링과정 발표할때 잘정리했는데 헷갈린다.. 나중에 다시 알아봐야겠다.)
// 로그인 관련 axios API 통신
// loginSlice
export const loginApi = {
  // 로그인
  login: data => instance.post("/auth/signin", data),
};
// 로그인
export const __login = createAsyncThunk(
  "post/login",
  async (data, thunkAPI) => {
    try {
      const response = await loginApi.login(data);
      console.log(response);
      if (response.status === 201) {
        localStorage.setItem("accessToken", response.data.member.accessToken);
        localStorage.setItem("refreshToken", response.data.member.refreshToken);
        window.location.replace("/signupFinish");
        return response.data;
      }
    } catch (err) {
      console.log(err);
      return thunkAPI.rejectWithValue(err.response.data);
    }
  },
);

요청마다 token의 만료기한을 확인하고 지났다면..

  • 이제는 핵심인 refreshToken이 나올차례이다.
  • 뭐 막 이리저리 적었지만 까고보면 별 거 없다.
  • 핵심은 react-jwt의 isExpired를 이용해서 만료를 확인하고
  • 만료라면 서버와 약속된 api로 미리 받은 refreshToken을 보내고 다시 accessToken을 받은 이후, 저장하고, 다시 보내면 된다.
import { isExpired } from "react-jwt";

//┏----------interceptor를 통한 header 설정----------┓
instance.interceptors.request.use(async config => {
  config.headers["Content-Type"] = "application/json; charset=utf-8";
  config.headers["X-Requested-With"] = "XMLHttpRequest";
  config.headers["Accept"] = "*/*";

  const accessToken = localStorage.getItem("accessToken");
  if (!accessToken) return config;
	
	//////////////////추가된부분 시작//////////////////
  if (isExpired(accessToken)) {
    try {
      const refreshToken = localStorage.getItem("refreshToken");
      const response = await axios.post(
        process.env.REACT_APP_AUTH_SERVER + `/auth/token`,
        { refreshToken },
      );
      localStorage.setItem("accessToken", response.data.member.accessToken);
      // 12/21 추가 시작
      localStorage.setItem("refreshToken", response.data.member.refreshToken);
      // 12/21 추가 끝
      config.headers.Authorization = `Bearer ${response.data.member.accessToken}`;
      return config;
    } catch (error) {
      console.log(error);
      localStorage.clear();
      window.location.replace("/login");
      return config;
    }
  }
  //////////////////추가된부분 끝//////////////////

  config.headers.Authorization = `Bearer ${accessToken}`;
  return config;
});
  • 코드를 보면 바로 이해가 될 것이다.(아니라면..ㅠ)
  • 혹시나 낯설 수 있는 부분은 interseptors중에 axios.post로 한번더 api호출을 한다는 점인데
  • 이제 익숙해지면 된다.
  • +) 12/21 추가 -> 액세스토큰이 만료되고 재발급을 받고 다시 액세스토큰이 만료되면 오류가 나는 이슈 확인했습니다. 원인은 리프레쉬토큰을 이용해 토큰을 재발급 받을때 액세스토큰만을 새롭게 업데이트해서였으며 이제 리프레쉬토큰도 매순간 업데이트하기에 해당 오류는 나지 않습니다.

끝내며

  • 이렇게 refreshToken정리를 끝냈다.
  • 항해때는 진짜 어렵고 복잡해보였는데
  • 실타래를 하나씩 풀어보니까 마냥 어렵지만도 않은 것 같다.(?)
  • 다음에는 OAuth - kakao login을 하면 좋을 것 같다 :)

출처

https://velog.io/@skyepodium/axios-인터셉터로-API-관리하기

https://inpa.tistory.com/entry/AXIOS-📚-설치-사용

https://patrick-f.tistory.com/13

https://velog.io/@___pepper/React-OAuth-2.0-사용하기-refresh-token-grant

https://onelight-stay.tistory.com/278

https://velog.io/@gth1123/쿼리-스트링Query-string-URL-파라미터

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Date/now

http://daplus.net/javascript-axios에서-헤더와-옵션을-설정하는-방법은-무엇입니/

https://jacobgrowthstory.tistory.com/44

https://velog.io/@bigbrothershin/Axios-delete-요청-시-body에-데이터-넣는-법

https://grepper.tistory.com/72