[리팩토링] 로그인과 토큰 저장

soo's·2023년 7월 12일
0

Project - Danim(다님)

목록 보기
1/1
post-thumbnail

0. 기존 코드의 문제점

다님 서비스 프로젝트에서 로그인과 토큰 저장은 아래와 같이 사용함

  1. 로그인 시 서버로부터 액세스 토큰과 리프레쉬 토큰을 응답 헤더로 받음
  2. 클라이언트가 응답 헤더에서 토큰을 꺼내어 쿠키에 저장
  3. 인증 인가가 필요한 api 요청시 쿠키에서 꺼낸 액세스 토큰 사용
  4. 인증 인가 필요한 페이지 라우팅에서 액세스 토큰 검사하고(단순히 쿠키 존재 여부 검사) 존재하지 않는다면(=만료) 리프레쉬 토큰 가지고 액세스 재발급

코드 작성 이후 보안과 관련해서 문제점이 있다는 것을 알게됐음
일단 보안이 뚫리는 공격들에 대해서 간단하게 정리하자면

  • XSS(Cross Site Scripting) 공격
    : 사용자로부터 입력을 받는 폼 등에서 악성 스크립트가 삽입되어 해당 스크립트가 사용자의 브라우저에서 실행되게 하는 공격
    로그인시 사용자 이름이나 비밀번호 입력란에 악의적인 스크립트가 포함된 값을 입력하는 경우 악성 스크립트로 사용자 토큰 탈취
  • CSRF(Cross-site Request Forgery) 공격
    : 인증된 사용자의 권한을 이용하여 악의적인 요청을 보내는 공격

일단 기존의 코드에서 쿠키에 토큰을 저장하는 부분에서 악성 스크립트를 이용해서 토큰을 탈취당할 수 있음을 인지하게 됨
따라서 쿠키에 저장하지 않고 사용할 수 있게 코드를 수정하려고 함

1. 계획

  1. 응답 헤더로 전달받았던 액세스 토큰을 payload로 받기
  2. 액세스 토큰을 따로 저장하지 않고 axios의 instance의 header로 설정
  3. 리프레쉬 토큰은 httpOnly 설정을 통해 서버에서만 관리
  4. 액세스 토큰 만료시 인터셉터를 사용해서 재발급

이러한 수정 계획을 짰음
울팀 서버 개발자 영구님과 함께 시작~ 🙂

2. 시도들

2-1. 액세스 토큰 코드 변경


// 주요 코드 생략

// as-is
// 응답 헤더의 액세스 토큰과 리프레쉬 토큰을 쿠키에 저장함
const accessToken = response.headers.access_key;
const refreshToken = response.headers.refresh_key;
if (accessToken && refreshToken) {
   setCookie("accessToken", accessToken, 1);
   setCookie("refreshToken", refreshToken, 14);
   }

// to-be
// 응답의 payload로 액세스 토큰을 받아 인스턴스 헤더에 설정
const {accessToken, ...} = response.data.data;
axiosInstance.defaults.headers.common.ACCESS_KEY = `${accessToken}`;

먼저 액세스 토큰에 대한 코드를 변경했음
기존 코드에서는 응답 헤더에 있던 액세스 토큰과 리프레쉬 토큰을 쿠키에 저장했었음
하지만 header가 아닌 payload로 액세스 토큰을 전달받아서 바로 instance의 header로 설정함. 리프레쉬 토큰은 httpOnly 설정을 통해 서버에서만 관리하기 때문에 따로 코드를 작성하지 않음

2-1의 문제점

이렇게 변경하고 로그인을 시도해서 기타 토큰이 필요한 요청들을 테스트해봤는데 정상적으로 기능했음. 하지만 새로고침 시 401 에러 발생

원인은 웹 브라우저에서 페이지를 새로고침하면 JavaScript의 메모리가 초기화 되기 때문
따라서 JavaScript로 실행되는 모든 코드와 변수, 객체 등의 상태가 초기화 => axios 인스턴스와 그 헤더 설정도 초기화 😇

2-1의 해결방법

새로고침 할 때 토큰을 새로 재발급 받으면 되지 않을까 해서
가장 최상위 컴포넌트인 App.tsx가 마운트 될때 토큰 재발급 함수를 사용하기로 결정함

// APP.tsx

  // 새로고침 했을때 액세스 토큰 재발급
  useEffect(() => {
    refreshAccessToken();
  }, []);

// signUp.ts
export const refreshAccessToken = async () => {
  try {
    // 토큰 갱신 요청은 refreshInstance를 사용하여 보내기
    const response = await refreshInstance.get("/api Endpoint");
    const { accessToken } = response.data.data;
    axiosInstance.defaults.headers.common.ACCESS_KEY = `${accessToken}`;
    return accessToken;
  } catch (err: any) {
    console.log("여기서 에러", err);
    const errMessage = err.response.data.detail || err.message;
    return errMessage;
  }
};

0개의 댓글