Project/SeSAC 2차 팀 프로젝트

[새싹x코딩온] 웹 개발자 부트캠프 과정 2차 팀프로젝트 회고 #5 | 유저 권한 관리

다니니니 2024. 9. 22. 02:04
728x90

 

관리자 페이지까지 구현하고 팀 전체 진행도를 살펴보면서, 권한 관리의 필요성을 느꼈다.

권한이 없는 사용자가 경로를 직접 입력하여 관리자 페이지나 판매글 작성 페이지에 접근하는 것을 막아야 하기 때문이었다. 이런 접근제어를 통해 프로젝트의 신뢰성을 높이고자 했다.

 

 

useAuth라는 커스텀 훅을 만들어서 로그인하지 않은 사용자, 일반 유저, 판매자로 등록된 유저, 블랙리스트로 등록된 유저, 관리자 별로 접근을 제한하도록 구현했다.

import React, { createContext, useState, useEffect, useContext } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { loginFn } from '../store/loginSlice';

export const UserContext = createContext(null);

export function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const dispatch = useDispatch();
  const { isLogin, isAdmin, isSeller, isBlackList, headerMenu } = useSelector(
    (state) => state.login,
  );

  useEffect(() => {
    const storedUser = sessionStorage.getItem('user');
    const userInfo = JSON.parse(storedUser);
    if (userInfo) {
      dispatch(
        loginFn({
          isLogin: true,
          isAdmin: userInfo.admin || false,
          isSeller: userInfo.sellerId !== null ? true : false,
          isBlackList: userInfo.isBlacklist || false,
          headerMenu: userInfo.admin ? 'admin' : 'user',
        }),
      );
      if (!userInfo.admin) {
        // 일반 유저인 경우
        const { userId, sellerId, isBlacklist } = userInfo;
        if (!sellerId) {
          // 판매자가 아닌 경우
          dispatch(
            loginFn({
              isLogin: true,
              isAdmin: false,
              isSeller: false,
              isBlackList: isBlacklist,
              headerMenu: 'user',
            }),
          );
        } else {
          // 판매자인 경우
          dispatch(
            loginFn({
              isLogin: true,
              isAdmin: false,
              isSeller: true,
              isBlackList: isBlacklist,
              headerMenu: 'user',
            }),
          );
        }
      } else {
        // 관리자인 경우
        const { admin } = userInfo;
        dispatch(
          loginFn({
            isLogin: true,
            isAdmin: admin,
            isSeller: false,
            isBlackList: false,
            headerMenu: 'admin',
          }),
        );
      }
      setUser(userInfo);
      setLoading(false);
    }
  }, [dispatch]);

  const login = (userData) => {
    setUser(userData);
    setLoading(false);
    dispatch(
      loginFn({
        isLogin: true,
        isAdmin: userData.admin || false,
        isSeller: userData.sellerId ? true : false,
        isBlackList: userData.isBlacklist,
        headerMenu: userData.admin ? 'admin' : 'user',
      }),
    );
    sessionStorage.setItem('user', JSON.stringify(userData));
  };

  const logout = () => {
    setUser(null);
    setLoading(true);
    dispatch(
      loginFn({
        isLogin: false,
        isAdmin: false,
        isSeller: false,
        isBlackList: false,
        headerMenu: 'logout',
      }),
    );
    sessionStorage.removeItem('user');
  };

  // 판매자 등록
  const sellerRegister = (userData) => {
    setUser(userData);
    setLoading(false);
    dispatch(
      loginFn({
        isLogin: true,
        isAdmin: false,
        isSeller: true,
        isBlackList: false,
        headerMenu: 'user',
      }),
    );
    sessionStorage.setItem('user', JSON.stringify(userData));
  };

  return (
    <UserContext.Provider
      value={{ user, login, logout, loading, sellerRegister }}
    >
      {children}
    </UserContext.Provider>
  );
}

 

createContext를 이용해서 userContext라는 것을 만들고, 사용자 정보를 전역적으로 관리하고,

로그인 상태에 따라 다양한 사용자 유형(일반 사용자, 판매자, 블랙리스트, 관리자)에 대한 접근 권한을 제어했다.

 

UserProvider 컴포넌트에서 세션 스토리지에 저장된 사용자 정보를 불러와 Redux를 통해 로그인 상태를 업데이트했다.

이를 통해 사용자가 로그인할 때마다 해당 사용자 유형에 맞는 권한을 설정하고, 관리자는 관리자 페이지에 접근할 수 있도록 했다.

 

로그인과 로그아웃 기능을 통해 사용자 정보를 세션 스토리지에 저장하거나 제거하여, 페이지를 새로고침하더라도 상태가 유지되도록 설계했다. 또한, 판매자 등록 시에도 동일한 방식을 사용해서 유연한 인증 구조를 구축했다.

 

이렇게 리덕스에 저장된 유저의 권한 정보를 가지고 라우팅 컴포넌트를 구현해서 사용자의 권한에 따라 접근을 제한하고, 적절한 경로로 리다이렉트 하도록 했다.

1. ProtectedRoute

import React from 'react';
import { Navigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { UserContext } from '../../hooks/useAuth';

export default function ProtectedRoute({ element: Element, ...rest }) {
  const { isLogin, isAdmin, isSeller, isBlackList } = useSelector(
    (state) => state.login,
  );

  const { loading, user } = React.useContext(UserContext);

  if (loading) {
    return (
      <div className="loading">
        <div className="loading-inner"></div>
      </div>
    );
  }

  if (!isLogin) {
    return <Navigate to="/" />;
  }

  return <Element {...rest} />;
}

ProtectedRoute는 사용자의 로그인 여부를 확인했다. 로그인한 일반 유저 정보가 필요한 장바구니 페이지, 결제 페이지, 결제 완료 페이지, 마이페이지 등에 해당 컴포넌트를 연결했다. 

권한이 없는 사용자가 해당 페이지에 접근하려고 하면 메인페이지로 리다이렉트하게 했다.

 

{/* 장바구니 페이지 */}
<Route
  path="/cart"
  element={<ProtectedRoute element={CartPage} />}
/>
{/* 결제 페이지 */}
<Route
  path="/order"
  element={<ProtectedRoute element={OrderPage} />}
/>
{/* 결제 완료 페이지 */}
<Route
  path="/order/complete/:allOrderId"
  element={<ProtectedRoute element={OrderCompletePage} />}
/>

2. NonLoginRoute

회원가입 페이지같이 로그인하지 않았을 때만 접근할 수 있는 라우트를 정의했다.

로그인한 사용자가 경로로 직접 접근하면 메인페이지로 리다이렉트 된다.

import React from 'react';
import { Navigate } from 'react-router-dom';
import { useSelector } from 'react-redux';

export default function NonLoginRoute({
  element: Element,
  requiredRole,
  ...rest
}) {
  const { isLogin } = useSelector((state) => state.login);

  if (isLogin) {
    return <Navigate to="/" />;
  }

  return <Element {...rest} />;
}

3. SellerRoute

import React from 'react';
import { Navigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { UserContext } from '../../hooks/useAuth';

export default function SellerRoute({ element: Element, ...rest }) {
  const { isLogin, isAdmin, isSeller, isBlackList } = useSelector(
    (state) => state.login,
  );

  const { loading, user } = React.useContext(UserContext);

  if (loading) {
    return (
      <div className="loading">
        <div className="loading-inner"></div>
      </div>
    );
  }

  if (!isLogin) {
    return <Navigate to="/" />;
  }

  if (!isSeller || isBlackList) {
    return <Navigate to="/" />;
  }

  return <Element {...rest} />;

판매자로 등록된 사용자만 접근할 수 있는 페이지(판매글 작성 페이지, 판매글 수정 페이지)에 라우트를 정의했다.

판매자로 등록되지 않았거나 블랙리스트로 등록된 사람이 경로로 직접 접근하려고 하면 메인 페이지로 리다이렉트 된다.

 

4. AdminRoute

import React from 'react';
import { Navigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { UserContext } from '../../hooks/useAuth';

export default function ProtectedRoute({ element: Element, ...rest }) {
  const { isLogin, isAdmin, isSeller, isBlackList } = useSelector(
    (state) => state.login,
  );

  const { loading, user } = React.useContext(UserContext);

  if (loading) {
    return (
      <div className="loading">
        <div className="loading-inner"></div>
      </div>
    );
  }

  if (!isLogin) {
    return <Navigate to="/" />;
  }

  if (!isAdmin) {
    return <Navigate to="/" />;
  }

  return <Element {...rest} />;
}
{/* 관리자페이지 */}
<Route path="/admin" element={<AdminRoute element={Admin} />}>
  <Route
    path="/admin"
    element={<AdminRoute element={AdminPage} />}
  />
  {/* 전체 회원 관리 */}
  <Route
    path="/admin/allUser"
    element={<AdminRoute element={AdminAlluserPage} />}
  />
  {/* 판매자 관리 */}
  <Route
    path="/admin/seller"
    element={<AdminRoute element={AdminSellerPage} />}
  />
  {/* 판매자 신고글 관리 */}
  <Route
    path="/admin/complaint/:sellerId"
    element={<AdminRoute element={AdminSellerComplaintPage} />}
  />
  {/* 블랙리스트 관리 */}
  <Route
    path="/admin/blacklist"
    element={<AdminRoute element={AdminBlacklistPage} />}
  />
  {/* 거래내역 관리 */}
  <Route
    path="/admin/orderlogs"
    element={<AdminRoute element={AdminOrderLogsPage} />}
  />
</Route>
</Route>

 

관리자 계정으로만 접근할 수 있는 페이지에 라우트를 정의했다.

그래서 관리자가 아닌 사용자가 경로로 직접 접근할 시 메인페이지로 리다이렉트되도록 구현했다.

 

이런 커스텀 훅과 라우팅 구성으로 사용자 권한에 따른 접근 제한을 효과적으로 구현할 수 있었다.

각 사용자 유형이 필요로 하는 정보와 기능만을 접근할 수 있도록 제한함으로써, 보안성을 강화할 수 있었다.

 

728x90