[React 완벽가이드] Section 22: 인증

gonn-i·2024년 7월 17일
0

React 완벽 가이드

목록 보기
17/18
post-thumbnail

본 포스트는 Udemy 리액트 완벽가이드 2024 를 듣고 정리한 내용입니다.

목차 🌳
1️⃣ 리액트 앱에서 유저의 권한을 부여하는 방법
2️⃣ 로그인 후, 백엔드로부터 받은 res에서 토큰 찾아 유저의 권한저장 및 유지 🔍
3️⃣ 유저의 권한 부여 이후, 백엔드에 req 보내기 📤
4️⃣ 유저의 권한 회수 및 로그아웃 방법 📭


리액트 앱에서 유저의 권한을 부여하는 방법

인증이란 무엇인가?

인증(권한 부여)를 통해,필요한 데이터를 보호하기 위해 사용
(내가 올린 글만을 수정하고 삭제, .. )
대개, 보호된 리소스에 접근하려는 미래의 요청에 서버 측의 세션이나, 인증 토큰을 사용

1️⃣ server-side session
서버에서 뿌려준 식별자를 저장해, 이를 클라이언트로 보내주는 방법으로, 세션을 이용하기 위해서는 프론트와 백의 긴밀한 연결이 선행되어야 한다, (그치만 react 는 분리된 백엔드 api를 주로 사용하기 때문에 토큰 사용이 권장됨)

2️⃣ Authentiation token
토큰을 이용할 경우, 서버로 부터 생성된 인증 토큰을 (저장하지 않고) 클라이언트로 전송한다
이후, 클라이언트에서 토큰을 req에 첨부해서 (header에 포함) 보내면 서버가 토큰의 유효성을 검사한다.

이전에 사용했던 이벤트 등록 사이트에 로그인과 회원가입 기능, 유저의 권한에 따른 삭제와 수정, 로그아웃을 구현해보며 권한에 따른 UI 변경과 http 요청을 알아보자!


로그인 후, 백엔드로부터 받은 res에서 토큰 찾아 유저의 권한저장 및 유지 🔍

로그인에서 postreq 를 보낸 이후, 받은 res 에서 token 을 추출해 이를 브라우저단에 저장한다! 보통 locastorage storagesession storage 등등을 사용하는데 취향에 따라 골라잡으면 될 듯하다. (물론 스토리지에 대한 선택은 브라우저를 뭘 쓰는냐에 따라 특정 웹 스토리지를 지원하지 않을 수도 있고, 데이터 지속성에도 차이가 있어 상황에 따라서도 더 생각해보자!)

영상에서는 localstorage 를 사용했고 토큰을 웹 단에서 저장하는 방법은 다음과 같았다.

Authentication.js

  // res 에 있는 토큰을 추출하여 사용 -> 보통 localstorage에 저장
  const resData = await res.json();
  const token = resData.token;
  localStorage.setItem('token', token);

또한, JWT를 통한 토큰 생성에 있어서, 토큰에는 만료시간이 존재하는데 이에 따라 클라이언트쪽에서도 localstorage에 저장한 토큰을 제거해줘야 한다! 그래서 생성된 토큰에 대한 만료시간을 같이 저장하면서 권한을 업데이트 해준다.

Authentication.js

  // 현재 시간을 추출한다.
  const expiration = new Date();
  // 현재 시간 + 1시간을 더한 값을 계산한다.
  expiration.setHours(expiration.getHours() + 1);
  // 날짜 객체를 ISO 8601 형식의 문자열로 변환 -> localstorage 에 저장하기
  localStorage.setItem('expiration', expiration.toISOString());

이렇게 클라이언트 측에서 보관한 정보들은 util 폴더안에서 그 값들을 꺼내보며 토큰이 존재하는지, 토큰이 만료되지는 않았는지를 판별한다.

utils -> auth.js

export function getAuthToken() {
  const token = localStorage.getItem('token');

  //토큰이 없는 경우,
  if (!token) {
    return;
  }
  const tokenDuration = getTokenDuration();

  // 토큰이 만료된 경우 
  if (tokenDuration < 0) {
    return 'EXPIRED';
  }
  // 토큰이 존재하는 경우
  return token;
}

export function getTokenDuration() {
  // localstorage에 저장된 만료 시간을 꺼내어, 만료시간 - 현재시간을 뺀 값을 리턴
  const storedExpirationDate = localStorage.getItem('expiration');
  const expiration = new Date(storedExpirationDate);
  const now = new Date();
  const duration = expiration.getTime() - now.getTime();
  return duration;
}

또한 여기에loader로 걸어줄 함수들도 선언해준다!

// loader로 걸어줄 토큰 추출 함수
export function tokenLoader() {
  return getAuthToken();
}

// token 이 있어야만 특정 페이지에 도달할 수 있도록 판별
export function checkAuthLoader() {
  const token = getAuthToken();

  if (!token) {
    return redirect('/auth');
  }

  return null;
}

router에 권한이 필요한 곳에 checkAuthLoader 을 걸어주어, URL 을 통한 유입을 막고,
getAuthToken 을 통해 token 값의 유무에 따라 Nav 에서 노출되어야 할 정보를 조절하면 된다

routerloader 어떻게 걸어주는지 잘보고 익히기 👀
app.js

// 라우트 보호 
const router = createBrowserRouter([
  {
    path: '/',
    element: <RootLayout />,
    errorElement: <ErrorPage />,
    id: 'root',
    loader: tokenLoader, ⭐️
    children: [
      { index: true, element: <HomePage /> },
      {
        path: 'events',
        element: <EventsRootLayout />,
        children: [
          {
            index: true,
            element: <EventsPage />,
            loader: eventsLoader,
          },
          {
            path: ':eventId',
            id: 'event-detail',
            loader: eventDetailLoader,
            children: [
              {
                index: true,
                element: <EventDetailPage />,
                action: deleteEventAction,
              },
              {
                path: 'edit',
                element: <EditEventPage />,
                action: manipulateEventAction,
                loader: checkAuthLoader,  ⭐️
              },
            ],
          },
          {
            path: 'new',
            element: <NewEventPage />,
            action: manipulateEventAction,
            loader: checkAuthLoader,  ⭐️
          },
        ],
      },
	// ... 생략 
    ],
  },
]);

MainNav.js

import { Form, NavLink, useRouteLoaderData } from 'react-router-dom';
// 생략 ...

function MainNavigation() {
  // root 에 걸어둔 loader 함수의 반환값 가져오기 
  const token = useRouteLoaderData('root');

  return (
    <header className={classes.header}>
      <nav>
        <ul className={classes.list}>
          // 생략 ...
          {!token && (
            <li>
              <NavLink to="/auth?mode=login" className={({ isActive }) => (isActive ? classes.active : undefined)}>
                auth
              </NavLink>
            </li>
          )}
          {token && (
            <li>
              <Form action="/logout" method="post">
                <button>Logout</button>
              </Form>
            </li>
          )}
        </ul>
      </nav>
    </header>
  );
}

export default MainNavigation;

유저의 권한 부여 이후, 백엔드에 req 보내기 📤

백엔드에서 토큰의 유효성을 검사하고 특정 api를 실행할 수 있도록 설계했다면, 클라이언트쪽에서는
req를 보낼때, header 에 Authentication 값으로 Bearer + token 값을 함께 넣어주어야 한다! (localstorage 에 저장한 값을 빼서, 같이 req로 날려버려 ~)

req 보내는 형식 예시

// 📁 utils -> auth.js 에서 만들어놓은 함수에서 token 값 반환 받기
  const token = getAuthToken();
  
  // 생략...
  const response = await fetch(url, {
    method: method,
    headers: {
      'Content-Type': 'application/json',
      Authorization: 'Bearer ' + token, // ⭐️
    },
    body: JSON.stringify(eventData),
  });

유저의 권한 회수 및 로그아웃 방법 📭

유저는 2가지 경우에 권한을 잃게 된다.
1️⃣ 로그아웃 버튼 누르기
2️⃣ 시간 만료로 인한, 토큰 만료

하나씩 처리 방법을 알아보자

1️⃣ 로그아웃 버튼 누르기

1) action에서 localstorage 에 저장한 토큰정보와 만료시간을 제거하고, root 페이지로 리다이렉트를 건다

//components -> Logout.js
import { redirect } from 'react-router-dom';

export function action() {
  localStorage.removeItem('token');
  localStorage.removeItem('expiration');
  return redirect('/');
}

2) router 에 위에서 설정한 action 걸어두기

// app.js
{
  path: '/logout',
    action: logoutAction,
},

3) 클릭하면 걸어둔 action 함수가 호출되어 로그아웃이 이루어지도록 설정

// MainNav.jsx
{token && (
  <li>
    <Form action="/logout" method="post">
      <button>Logout</button>
    </Form>
  </li>
)}

2️⃣ 시간 만료로 인한, 토큰 만료

root page 에서 token 값을 통해, 자동 로그아웃 처리

function RootLayout() {
  const token = useLoaderData();
  const submit = useSubmit();
  
  useEffect(() => {
    if (!token) {
      return;
    }
    
    // 토큰이 만료된 경우, 로그아웃 처리
    if (token === 'EXPIRED') {
      submit(null, { action: '/logout', method: 'post' });
    }

    // 토큰 만료시간에 따라, 타이머 on 설정 후 시간 도달시 자동 로그아웃
    const tokenDuration = getTokenDuration();
    setTimeout(() => {
      submit(null, { action: '/logout', method: 'post' });
    }, tokenDuration);
  }, [token, submit]);
  
  return (
    <>
		// ... 생략
    </>
  );
}

0개의 댓글