반응형
250x250
Notice
Recent Posts
Recent Comments
Link
관리 메뉴

짧은코딩

axios interceptors를 활용하여 JWT 관리하기 with MSW2 본문

UpLog 릴리즈노트 프로젝트

axios interceptors를 활용하여 JWT 관리하기 with MSW2

5_hyun 2024. 2. 13. 09:21

JWT 관리 방식

이 프로젝트에서는 권한 문제가 많기 때문에 JWT 관리를 어떻게 할지 더 고민했다.

https://hshine1226.medium.com/localstorage-vs-cookies-jwt-%ED%86%A0%ED%81%B0%EC%9D%84-%EC%95%88%EC%A0%84%ED%95%98%EA%B2%8C-%EC%A0%80%EC%9E%A5%ED%95%98%EA%B8%B0-%EC%9C%84%ED%95%B4-%EC%95%8C%EC%95%84%EC%95%BC%ED%95%A0-%EB%AA%A8%EB%93%A0%EA%B2%83-4fb7fb41327c

 

LocalStorage vs. Cookies: JWT 토큰을 안전하게 저장하기 위해 알아야할 모든것

안녕하세요 백엔드 개발자 최준혁입니다.

hshine1226.medium.com

이 글을 보고 accessToken은 메모리에 두고, refreshToken은 쿠키에 저장하는 방식으로 했다.

MSW2 설정

-로그인과 accessToken, refreshToken 재발급 api

  http.post('/members/refresh', () => {
    return HttpResponse.json(
      { accessToken: 'MSW-new-accessToken' },
      {
        headers: {
          'Set-Cookie': 'refreshToken=MSW-refreshToken;Max-Age=999999999999;',
        },
        status: 200,
      }
    );
  }),
  http.post('/members/login', (info) => {
    const infos: LoginInfo = info.request.json();

    if (infos.password === '1234') {
      return HttpResponse.json({ httpStatus: 'CONFLICT', message: '비밀번호가 틀립니다.' });
    }

    const position = infos.email === 'company@gmail.com' ? 'COMPANY' : 'INDIVIDUAL';
    const data: GetUserInfo = {
      id: faker.number.int(),
      image: null,
      email: infos.email,
      name: '권오현',
      nickname: '오리',
      position: position,
      accessToken: 'MSW-accessToken',
    };

    return HttpResponse.json(data, {
      headers: {
        'Set-Cookie': 'refreshToken=MSW-refreshToken;Max-Age=999999999999;',
      },
    });
  }),
  • 로그인(/members/login)
    • 우선 로그인 mock api에서는 로그인을 하면 유저 정보, accessToken를 보내주고 cookie에 refreshToken을 설정했다.
  • refresh(/members/refresh)
    • refresh mock api에서는 새로운 accessToken과 refreshToken을 보내줬다.

-제품 리스트를 조회하는데 accessToken, refreshToken이 만료된 경우

import { DefaultBodyType, http, HttpResponse, StrictRequest } from 'msw';

export const checkAuthorization = ({
  cookies,
  request,
}: {
  cookies: Record<string, string>;
  request: StrictRequest<DefaultBodyType>;
}) => {
  const { refreshToken } = cookies;

  if (refreshToken !== 'MSW-refreshToken') {
    return new HttpResponse(null, {
      status: 410,
    });
  }

  const accessToken = request.headers.get('authorization')?.slice(7) ?? '';

  if (accessToken !== 'MSW-new-accessToken') {
    return new HttpResponse(null, {
      status: 409,
    });
  }

  // 인증이 성공하면 null을 반환
  return null;
};

checkAuthorization 함수는 accessToken, refreshToken을 체크해주는 함수이다.

import { http, HttpResponse } from 'msw';
import { faker } from '@faker-js/faker';
import { createProduct } from '@/mocks/api/data/product';
import { checkAuthorization } from '@/mocks/api/common.ts';

export const product = [
  http.get('/products', async ({ cookies, request }) => {
    // 함수 호출
    const authCheckResult = checkAuthorization({ cookies, request });

    // 만약 인증에 실패한 경우
    if (authCheckResult !== null) {
      return authCheckResult;
    }

    const products = faker.helpers.multiple(createProduct, {
      count: faker.number.int({ min: 0, max: 5 }),
    });

    for (let i = 0; i < products.length; i++) {
      products[i].indexNum = i + 1;
    }

    return HttpResponse.json(products);
  }),
];
  • refreshToken이 만료된 경우
    • refreshToken이 만료되면 410 상태 코드를 보내준다.
  • accessToken이 만료된 경우
    • header에 들어있는 accessToken이 만료되면 409 상태 코드를 보내준다.

axios interceptors 설정

-초기 설정

import axios from 'axios';

let accessToken: string;

export const instance = axios.create({
  withCredentials: true,
});

export async function reissuanceJwt() {
  const res = await axios.post(`/members/refresh`, null, { withCredentials: true });
  return res;
}
  • accessToken 변수 메모리에 accessToken을 저장하는 방식으로 설정
  • instance에 axios.create를 이용하여 커스텀
    • withCredentials: true를 하여 쿠키를 공유
  • reissuanceJwt 함수
    • accessToken이 만료되면 reissuanceJwt 함수를 사용하여 갱신

-응답 처리 interceptors

instance.interceptors.response.use(
  // 200번대 응답이 올때 처리
  (response) => {
    return response;
  },
  // 200번대 응답이 아닐 경우 처리
  async (error) => {
    const {
      config,
      response: { status },
    } = error;

    //토큰이 만료되을 때
    if (status === 409) {
      const originRequest = config;
      const response = await reissuanceJwt();

      //리프레시 토큰 요청이 성공할 때
      if (response.status === 200) {
        const newAccessToken = response.data.accessToken;
        accessToken = newAccessToken;

        //진행중이던 요청 이어서하기
        originRequest.headers.Authorization = `Access=${newAccessToken}`;
        return axios(originRequest);
        //리프레시 토큰 요청이 실패할때(리프레시 토큰도 만료되었을때 = 재로그인 안내)
      }
    } else if (status === 410) {
      sessionStorage.removeItem('userInfo');
      window.location.replace('/login');
    }
    return Promise.reject(error);
  }
);
  • 200번대가 오면 그냥 그대로 처리
  • error 발생 시
    • 409 에러
      1. accessToken이 만료됐는데 refreshToken이 유효
      2. 갱신에 성공하여 200 상태 코드가 오면
      3. accessToekn 갱신하고 진행 중이던 요청 이어서 진행
    • 410 에러
      • refreshToken이 만료되었기에 로그아웃

-요청 처리 interceptors

instance.interceptors.request.use(
  (config) => {
    if (accessToken) {
      config.headers.Authorization = `Access=${accessToken}`;
    }

    return config;
  },
  (error) => Promise.reject(error)
);
  • accessToken을 header에 담고 요청을 보냄

-전체 코드

import axios from 'axios';

let accessToken: string;

export const instance = axios.create({
  withCredentials: true,
});

export async function reissuanceJwt() {
  const res = await axios.post(`/members/refresh`, null, { withCredentials: true });
  return res;
}

// 토큰을 함께보내는 privateApi에 interceptor를 적용합니다
instance.interceptors.response.use(
  // 200번대 응답이 올때 처리
  (response) => {
    return response;
  },
  // 200번대 응답이 아닐 경우 처리
  async (error) => {
    const {
      config,
      response: { status },
    } = error;

    //토큰이 만료되을 때
    if (status === 409) {
      const originRequest = config;
      const response = await reissuanceJwt();

      //리프레시 토큰 요청이 성공할 때
      if (response.status === 200) {
        const newAccessToken = response.data.accessToken;
        accessToken = newAccessToken;

        //진행중이던 요청 이어서하기
        originRequest.headers.Authorization = `Access=${newAccessToken}`;
        return axios(originRequest);
        //리프레시 토큰 요청이 실패할때(리프레시 토큰도 만료되었을때 = 재로그인 안내)
      }
    } else if (status === 410) {
      sessionStorage.removeItem('userInfo');
      window.location.replace('/login');
    }
    return Promise.reject(error);
  }
);

instance.interceptors.request.use(
  (config) => {
    if (accessToken) {
      config.headers.Authorization = `Access=${accessToken}`;
    }

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

최종 결과

  1. 409 에러가 발생
  2. /members/refresh로 다시 갱신
  3. refreshToken이 만료가 안됐으면 성공하고 200번 상태 코드가 옴
  4. 진행 중이던 처리 계속 진행
728x90
반응형
Comments