삼중 로그인 권한

안승찬·2024년 11월 24일

CareMinder

목록 보기
1/1

안녕하세요.
팀 케어마인더에서 활동하고 있는 안승찬입니다.
오늘은 실제 병원에서 프로덕션으로 사용예정인 케어마인더 시스템에 로그인 구축을 하면서 사용한 방법에 대해 공유 드릴려고 합니다.

"권한 안에 권한 안에 권..."

병동 권한 (Ward Tablet)

병동 태블릿에 해당하며, 직접 병동에서 사용하는 기본 권한입니다.

스태프 권한 (Staff)

간호사 계정에 해당하며, 여러 병동을 관리할 수 있는 권한을 가집니다. 각 간호사가 담당 병동을 컨트롤 할 수 있습니다.

어드민 권한 (Admin)

간호사 계정을 관리하고 컨트롤할 수 있는 상위 권한입니다. 여러 간호사를 동시에 관리할 수 있는 기능이 포함됩니다.

저희는 JWT 로그인 방식을 사용하고 있습니다.
간략하게 JWT 로그인 방식을 첨부 하겠습니다.

이런 식으로 accessToken과 refreshToken을 관리해야 하는 상황에서, 저희는 권한을 세 가지(WARD, STAFF, ADMIN)로 분류했습니다.
이로 인해 각 API 요청마다 해당 권한에 맞는 토큰이 필요했으며, 이를 효율적으로 관리하기 위한 별도의 로직이 필요했습니다.

"내가 만든 쿠키,,"

먼저, 로그인 시 각 권한에 따라 쿠키에 토큰을 저장하는 방식으로 설계를 진행했습니다.
특히, 스태프 계정(STAFF)의 경우 병동 계정(WARD)으로 로그인한 뒤 특정 페이지에 접근할 때 추가적인 권한이 필요하기 때문에, WARD와 STAFF 두 종류의 토큰이 동시에 관리되도록 구현했습니다.

*WARD접근

*STAFF접근

*ADMIN접근

로그인 시 각 권한에 맞는 accessToken("권한") 형식으로 별도로 저장하여 관리했습니다.

그 후, 유저 권한을 Recoil 상태에 저장하는 방식으로 설계를 진행하였고, 초기에는 "WARD", "STAFF", "ADMIN" 으로 구분하여 각 API 요청 시 해당 권한에 맞는 토큰을 담아 보내는 방식으로 구현했습니다.

"문제야 문제..."

권한이 3개로 나누어지고 요청의 복잡성이 증가하면서 다른 팀원들이 작업 중에 API 요청 시 권한을 잘못 맵핑하는 경우가 잦아졌고, 코드 변경 시 모든 요청을 일일이 수정해야 하는 문제가 발생했습니다.

이 문제를 해결하기 위해, axios.interceptors를 사용하여 의존성을 줄이고, API 요청 시 자동으로 권한에 맞는 토큰을 추가하는 방식으로 로직을 수정하기로 계획했습니다.

"아따 마 거의 다왔다.."

저희는 Recoil을 통해 유저의 권한을 저장하고 있었기 때문에, 저장된 권한을 기반으로 API 요청 시 자동으로 해당 권한을 맵핑하여 요청을 보내는 방식으로 구현했습니다.
이렇게 함으로써, 각 요청에 필요한 권한을 동적으로 처리하고, 권한에 맞는 accessToken을 자동으로 설정하여 API 요청을 보낼 수 있게 되었습니다.

*실제 API 요청을 보내는 코드

axiosInstance.interceptors.request.use(
  (config: InternalAxiosRequestConfig<any>) => {
    let token = "";
    const userState = localStorage.getItem("recoil-persist");

    let userType = "";
    if (userState) {
      const userStateObj = JSON.parse(userState as string);
      userType = userStateObj.userState.type;
    }

    switch (userType) {
      case "WARD":
        token = Cookies.get("accessTokenWard") as string;
        break;
      case "STAFF":
        token = Cookies.get("accessTokenStaff") as string;
        break;
      case "ADMIN":
        token = Cookies.get("accessTokenAdmin") as string;
        break;
      default:
        null;
    }

    if (token) {
      if (!config.headers) {
        config.headers = new AxiosHeaders();
      }
      (config.headers as AxiosHeaders).set("Authorization", `Bearer ${token}`);

      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  error => {
    return Promise.reject(error);
  },
);

*토큰 유효성

axiosInstance.interceptors.response.use(
  res => {
    return res;
  },
  async error => {
    if (error.response && error.response.status === 401) {
      const userState = localStorage.getItem("recoil-persist");
      let userType = "";

      if (userState) {
        const userStateObj = JSON.parse(userState as string);
        userType = userStateObj.userState.type;
      }

      let refreshEndpoint = "";
      let accessTokenKey = "";
      let refreshTokenKey = "";

      switch (userType) {
        case "WARD":
          accessTokenKey = "accessTokenWard";
          refreshTokenKey = "refreshTokenWard";
          refreshEndpoint = `${SEVER_URL}/users/refresh-token`;
          break;
        case "STAFF":
          accessTokenKey = "accessTokenStaff";
          refreshTokenKey = "refreshTokenStaff";
          refreshEndpoint = `${SEVER_URL}/users/refresh-token`;
          break;
        case "ADMIN":
          accessTokenKey = "accessTokenAdmin";
          refreshTokenKey = "refreshTokenAdmin";
          refreshEndpoint = `${SEVER_URL}/users/refresh-token`;
          break;
        default:
          return Promise.reject(error);
      }

      try {
        const response = await axios.post(refreshEndpoint, {
          accessToken: Cookies.get(accessTokenKey),
          refreshToken: Cookies.get(refreshTokenKey),
        });

        if (!response.data) {
          return Promise.reject(new Error("토큰 리프레쉬 실패"));
        }

        Cookies.set(accessTokenKey, response.data.accessToken);
        Cookies.set(refreshTokenKey, response.data.refreshToken);

        // 원래 요청 재시도
        error.config.headers.Authorization = `Bearer ${response.data.accessToken}`;
        return axiosInstance.request(error.config);
      } catch (refreshError) {
        return Promise.reject(refreshError);
      }
    }

    return Promise.reject(error);
  },
);

"그래서 결과는?"

의존성을 줄인 결과, 추후에 개발되는 기능들에서 API 권한을 신경 쓸 필요가 없어졌습니다.
이제는 axios.interceptor가 자동으로 권한을 맵핑해주기 때문에, 개발자는 API 요청 시 권한을 신경 쓸 필요 없이 작업을 진행할 수 있습니다.
이로 인해 팀원들의 작업 속도가 향상되었고, 동시에 권한 관리와 시스템 안전성도 크게 개선되었습니다.

0개의 댓글