팀프로젝트: Wingle(1.0) API 연결 - Auth: 인증 토큰 관리

윤뿔소·2023년 4월 14일
0

팀프로젝트: Wingle

목록 보기
3/16

전편에서 보았듯이 세팅 끝났고, 이제 서로 맡은 바에 최선을 하기 시작한다!

나는 이제 인증 관련 개발을 맡았다. 로그인, 회원가입, 토큰 관리 등을 맡으려고 하고있다.

토큰 관리

우선 토큰을 어떻게 처리해야할지 생각해보자.

백엔드에서 토큰 방식은 JWT로 보내주신다. 액세스토큰은 30분, 리프레쉬토큰은 7일로 유효기간을 잡고 있다. 리프레쉬 토큰은 로컬스토리지에 넣고, 액세스토큰은 페이로드에 사용자 정보인 이메일이 들어가 있어 이거는 노출이 안되게 로컬 변수에 넣어야하나 싶었지만 일단은 로컬스토리지로 하기로 협의했다.

로컬스토리지

간단하다. 로컬스토리지에 넣고, 읽어올 함수만 구현한다.

// utils/accessTokenHandler.ts
export const saveAccessTokenToLocalStorage = (accessToken: string) => {
  if (typeof window !== "undefined") {
    localStorage.setItem("accessToken", accessToken);
  }
};

export const getAccessTokenFromLocalStorage = () => {
  if (typeof window !== "undefined") {
    return localStorage.getItem("accessToken") || "";
  }
};

// utils/refreshTokenHandler.ts
export const saveRefreshTokenToLocalStorage = (refreshToken: string) => {
  if (typeof window !== "undefined") {
    localStorage.setItem("refreshToken", refreshToken);
  }
};

export const getRefreshTokenFromLocalStorage = () => {
  if (typeof window !== "undefined") {
    return localStorage.getItem("refreshToken") || "";
  }
};

이렇게 하고, save는 로그인할 때와 리프레쉬로 액세스 토큰 가져올 때 쓸거고, get은 이제 권한이 필요할 때, header나 로그인 됐는지 안됐는지의 조건에 쓸 것이다.

axios interceptor

이제 토큰 저장 관리는 끝냈다. 우리 웹의 특징은 로그인을 해야 메인으로 넘어갈 수 있는, 보안성이 중요한 커뮤니티라 매 요청마다 이 토큰이 맞는지 확인하는 단계를 거쳐야한다.

interceptor를 가져와 전편에서 만들었던 axios instance에 요청 및 응답으로 가져올 수 있게 만들어보자.

request interceptor

요청 하기 전 가로채보자.

instance.interceptors.request.use(
  async (config) => {
    const accessToken = getAccessTokenFromLocalStorage();

    // 만료되지 않은 access token이 있는 경우에는 해당 토큰을 사용
    if (accessToken !== null) {
      config.headers.Authorization = accessToken;
    }

    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

request 인터셉터를 만들었다. config를 만들어서 매 요청에 토큰을 넣으려고 했다.

getAccessTokenFromLocalStorage() 함수를 이용하여 localStorage에 저장된 access token을 가져온 뒤, 요청 헤더에 Authorization 필드로 첨부했다.
물론 instance에 토큰을 헤더에 담아 보내는 초기 세팅을 했지만 혹시 모를 오류와 최신화된 액세스토큰을 반영하기 위해 요청 시 토큰을 담도록 만들었다.

reponse interceptor

여기가 가장 중요한 포인트다. 먼저 기능을 어떻게 만들지 생각했냐면

  1. 응답 전 가로채서 res 그대로인지, error가 오는지를 체크
  2. error 시 액세스토큰이 만료됐다는 status인 401이 뜨면은 save, 404면 로컬스토리지 청소 및 다시 login 하게 만들게.

이렇게 하려고 했다. 이제 구현해보자.

instance.interceptors.response.use(
  (response) => {
    return response;
  },
  async (error) => {
    const { config, response } = error;

    // 에러 응답이 없으면 error 리턴 - 다른 에러로 response.status 가 없을 수 있음.
    if (!response) {
      return Promise.reject(error);
    }

    // 리프레시 토큰으로 엑세스 토큰 재발급
    const accessToken = await getAccessToken();

    if (response.status === 401) {
      saveAccessTokenToLocalStorage(`Bearer ${accessToken}`);
      config.headers.Authorization = `Bearer ${accessToken}`;
      return axios(config);
    }

    if (response.status === 404) {
      localStorage.clear();
      window.location.href = "/login";
    }
    return Promise.reject(error);
  }
);

use는 총 2개의 인자를 받는다. 성공 시, 실패 시.

  1. 성공하면 바로 응답 전달하고 에러가 났으면 그 에러의 응답데이터를 가져와 분석한다.
  2. 에러시 일단 액세스토큰을 받기
  3. 401 시 토큰이 만료됐으니, 발급받은 새로운 엑세스 토큰을 로컬 스토리지에 저장하고, 요청 헤더에 새로운 엑세스 토큰을 담아 요청을 재시도한다.
  4. 404면 일단 뭔가가 잘못 됐으니 다시 로그인으로 돌아가기

이렇게 만들었다. 이제 getAccessToken()을 구현해보자.

// 기능 : 리프레시 토큰으로 엑세스 토큰 재발급
const getAccessToken = async () => {
  try {
    const refresh = getRefreshTokenFromLocalStorage();

    // null 처리
    if (refresh === null) {
      throw new Error("리프레시 토큰이 존재하지 않습니다.");
    }

    const response = await instance.get("/auth/refresh", {
      headers: {
        refreshToken: refresh,
      },
    });

    // 토큰 재발급에 성공한 경우
    if (response.status === 200) {
      const {
        data: { grantType, accessToken, refreshToken },
      } = response;

      // 새로운 access token을 로컬 스토리지에 저장
      saveAccessTokenToLocalStorage(`${grantType} ${accessToken}`);

      // 새로운 refresh token이 발급된 경우에는 해당 토큰도 로컬 스토리지에 저장
	  saveRefreshTokenToLocalStorage(`${refreshToken}`);

      return accessToken;
    }

    // refresh token이 만료된 경우 1개
    if (response.status === 404) {
      throw new Error(`${response.data.message}`);
    }
    // 그 외의 경우에는 에러를 발생시킴
    throw new Error(`토큰 재발급에 실패했습니다. 상태 코드: ${response.status}`);
    
  } catch (error) {
    // 리프레시 토큰 만료 에러 핸들링 2개
    console.log("리프레시 토큰 오류", error);
    localStorage.clear();
    window.location.href = "/login";
  }
};

은근 길지만 뭐 별거 없다.

  1. 리프레쉬 변수 선언, 없다면 throw하여 catch로 넘어가게
  2. instance.get하여 헤더에 refreshToken을 담아줘 재발급 요청
  3. 요청에 따라 구현하고 리턴하기
    • 성공 시 액세스, 리프레쉬 토큰 다시 넣고 accessToken 리턴
    • 실패 시 throw
  4. 실패돼서 throw 됐다면 로컬스토리지 청소 및 /login으로 출발

이런 식으로 했다!


결론

interceptor를 이용하여 HTTP 통신할 때 주의할 점들을 고르기 위해 만들어 봤다. 이번이 처음이라 좀 어려웠는데 그래도 Example 보면서 하니 쉬운 편이었다. 다음엔 로그인 페이지를 만드는 과정에 대해서 기술 하긋다.

요약

  1. Axios 인스턴스를 생성하고, 서버 주소와 인증 정보를 기본 헤더에 설정.
  2. 인터셉터를 사용하여, 요청에 대한 처리. 인터셉터에서는 로컬 스토리지에서 Access Token을 가져와 헤더에 설정.
  3. 인터셉터를 사용하여, 응답에 대한 처리. 인터셉터에서는 만료된 Access Token일 경우, Refresh Token으로 Access Token을 갱신.
  4. Refresh Token으로 Access Token을 갱신하는 기능을 수행하는 함수 getAccessToken를 정의.
  5. Refresh Token이 만료된 경우, 로그아웃 처리를 하고 로그인 페이지로 이동.
profile
코뿔소처럼 저돌적으로

8개의 댓글

comment-user-thumbnail
2023년 4월 14일

메인때 로그인 부분 했었는데 디테일이 살발하네요 ㅜㅠ 잘 보고 갑니당

답글 달기
comment-user-thumbnail
2023년 4월 14일

AccessToken 관리하는 로직이 깔끔하네요. 다 읽어봤는데, 쉽지않네요 ㅎㅎ 화이팅입니다!

답글 달기
comment-user-thumbnail
2023년 4월 16일

엇 저는 계속 remove로 했는데 clear가 있었네요.. ! 덕분에 수정하러 가야겠습니다 ㅎ

1개의 답글
comment-user-thumbnail
2023년 4월 16일

if (typeof window !== "undefined") 조건을 넣어줘야 하는 이유가 뭔가요?!

1개의 답글
comment-user-thumbnail
2023년 4월 16일

clear..! 잘 읽고 갑니다

답글 달기
comment-user-thumbnail
2023년 4월 17일

공식 문서 보면서 그대로 따라하기도 쉽지는 않은건데, 그걸 쉽게 하시다니 내공이 어마무시하시군요! ㄷㄷ

답글 달기