[React] 인증토큰 활용하기

p-q·2023년 4월 16일
0

react

목록 보기
5/5

JWT(JSON web token) 소개

JWT는 토큰 인증 방식 중 하나로, JSON 객체를 사용하여 토큰을 생성하고 인증하는 방식입니다. 이 방식은 상대적으로 가벼운 구조를 가지고 있어 많은 웹 서비스에서 사용되고 있습니다.

Bearer

Bearer는 '소지자' 또는 '착용자'라는 뜻이며, 인증 토큰을 전달할 때 사용되는 방식입니다. 이 방식은 토큰 소지자가 해당 토큰을 사용하여 인증을 받을 수 있다는 의미로 사용됩니다.

토큰의 이해와 필요성

웹 서비스에서 사용자 인증을 처리하기 위해 토큰을 사용하는 방식이 널리 채택되고 있습니다. 토큰은 사용자의 정보와 권한을 저장하고 있는 일종의 문자열로, 서버와 클라이언트 간의 통신에 사용됩니다.

토큰 인증 방식

토큰 인증 방식은 사용자 인증 정보를 토큰으로 변환하여 전달하는 방식입니다. 인증 토큰은 일종의 문자열로, 서버와 클라이언트 간의 통신에 사용됩니다.

JWT 구성요소

JWT는 세 가지 구성요소로 이루어져 있습니다: 헤더(header), 페이로드(payload), 시그니처(signature). 헤더에는 토큰의 유형과 알고리즘 정보가 포함되어 있으며, 페이로드에는 사용자 정보와 권한 등이 담겨 있습니다. 시그니처는 서버에서 생성되어 토큰의 유효성을 검증하는 데 사용됩니다.

JWT 동작 방식

사용자가 로그인 또는 회원가입을 요청합니다.
서버에서 사용자 정보를 확인한 후 JWT를 생성합니다.
생성된 JWT를 클라이언트에게 전달합니다.
클라이언트는 전달받은 JWT를 저장하고, 인증이 필요한 요청에 함께 보냅니다.
서버는 전달받은 JWT를 검증하여 사용자 인증을 확인합니다.

내보내는 요청에 토큰 첨부하기

요청후 토큰 저장하기

export const action = async ({request, params}) => {
  const mode = new URL(request.url).searchParams.get('mode') || 'login';

  if(mode !== 'login' && mode !== 'signup'){
    throw json({message: 'Invalid mode'}, {status: '422'});
  }

  const data = await request.formData();
  const authData = {
    email: data.get('email'),
    password: data.get('password'),
  }

  const response = await fetch(`http://localhost:8080/${mode}`,{
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(authData),
  });

  if(response.status === 422 || response.status === 401){
    return response;
  }

  if(!response.ok){
    throw json({message: 'Could not authenticate user.'}, {status: 500});
  }

  const resData = await response.json();
  const token = resData.token;

  localStorage.setItem('token', token);

  return redirect('/');

}

요청 후 토큰을 저장하는 과정에서는 사용자의 로그인 또는 회원가입 요청을 받고, 인증에 성공하면 토큰을 생성하여 로컬 스토리지에 저장합니다. 이 과정은 위 코드에서 확인할 수 있습니다.

요청시 토큰 보내기

export const action = async ({ params, request }) => {
  const eventId = params.eventId;
  const token = getAuthTokens();
  const response = await fetch('http://localhost:8080/events/' + eventId, {
    method: request.method,
    headers: {
      Authorization: `Bearer ${token}`,
    },
  });

  if (!response.ok) {
    throw json(
      { message: 'Could not delete event.' },
      {
        status: 500,
      }
    );
  }
  return redirect('/events');
}

인증된 사용자가 특정 요청을 할 때, 예를 들어 이벤트를 삭제하고자 할 경우, 토큰을 함께 보내어 서버에서 사용자 인증을 확인합니다. 위 코드에서는 이벤트 삭제 요청에 토큰을 함께 보내는 과정을 확인할 수 있습니다.

사용자 로그아웃 처리

토큰제거

사용자 로그아웃을 처리하려면 먼저 사용자의 인증 토큰을 제거해야 합니다. 이를 위해 로그아웃 컴포넌트를 생성해봅시다.

로그아웃 컴포넌트 생성

import { redirect } from "react-router-dom";

export const action = () => {
  localStorage.removeItem('token');
  return redirect('/');
}

위 코드는 localStorage에 저장된 토큰을 제거한 뒤, 사용자를 홈페이지로 리다이렉트합니다.

인증상태에 따라 UI 상태 업데이트

auth 컴포넌트에 추가

export const getAuthTokens = () => {
  const token = localStorage.getItem('token');
  return token;
};

export const tokenLoader = () => {
  return getAuthTokens();
}

그리고 라우터 객체에 로더를 추가합니다.

라우터 객체에 추가

import { loader as tokenLoader } from './util/auth';
const router = createBrowserRouter([
  {
    path: '/',
    element: <RootLayout />,
    errorElement: <ErrorPage />,
    id: 'root',
    loader: tokenLoader,
    children: [
      { index: true, element: <HomePage /> },
		//...
    ],
  },
]);

위 코드는 loader()를 통해 데이터를 가져옵니다.

import { NavLink, Form, useRouteLoaderData } from "react-router-dom";

import classes from "./MainNavigation.module.css";
import NewsletterSignup from "./NewsletterSignup";

const MainNavigation = () => {
  const token = useRouteLoaderData("root");

  return (
    <header className={classes.header}>
      <nav>
        <ul className={classes.list}>
          <li>
            <NavLink to="/" className={({ isActive }) => (isActive ? classes.active : undefined)} end>
              Home
            </NavLink>
          </li>
          <li>
            <NavLink to="/events" className={({ isActive }) => (isActive ? classes.active : undefined)}>
              Events
            </NavLink>
          </li>
          <li>
            <NavLink to="/newsletter" className={({ isActive }) => (isActive ? classes.active : undefined)}>
              Newsletter
            </NavLink>
          </li>
          {!token && (
            <li>
              <NavLink to="/auth" className={({ isActive }) => (isActive ? classes.active : undefined)}>
                Authentication
              </NavLink>
            </li>
          )}
          {token && (
            <li>
              <Form action="/logout" method="post">
                <button>Logout</button>
              </Form>
            </li>
          )}
        </ul>
      </nav>
      <NewsletterSignup />
    </header>
  );
};

export default MainNavigation;

이제 인증 상태에 따라 사용자 인터페이스가 업데이트되어 로그아웃 버튼이 보이게 됩니다.

라우트 보호

리액트에서 라우트 보호는 사용자 인증을 확인하여 특정 페이지에 대한 접근 권한을 관리하는 것을 의미합니다. 이 글에서는 라우트 보호를 구현하는 방법에 대해 설명하겠습니다.

import { redirect } from 'react-router-dom';

export const getAuthTokens = () => {
  const token = localStorage.getItem('token');
  return token;
};

export const tokenLoader = () => {
  return getAuthTokens();
}

export const checkAuthLoader = () => {
  const token = getAuthTokens();

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

  return null;
}

checkAuthLoader 함수는 토큰이 없을 경우 리다이렉트 처리를 해주는 함수입니다. 토큰이 없으면 로그인 페이지로 리다이렉트합니다.

import { tokenLoader, checkAuthLoader } from "./util/auth";

const router = createBrowserRouter([
  {
    path: "/",
    element: <RootLayout />,
    errorElement: <ErrorPage />,
    id: "root",
    loader: tokenLoader,
    children: [
      { index: true, element: <HomePage /> },
      {
        path: "events",
        element: <EventsRootLayout />,
        children: [
			//..
          {
            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,
          },
        ],
      },
     //..
    ],
  },
]);

checkAuthLoader를 사용하여 라우트 보호를 적용합니다.

자동 로그아웃

인터넷에서 로그인 한 후, 일정 시간 동안 활동하지 않으면 자동으로 로그아웃되는 기능은 매우 중요합니다. 이것은 사용자의 개인 정보와 보안을 보호하며, 불필요한 서버 부하를 줄일 수 있습니다. React에서는 이러한 자동 로그아웃 기능을 구현하는 데 useEffect를 사용할 수 있습니다. 이 기능을 구현하는 방법에 대해 알아보겠습니다.

import { Outlet, useLoaderData, useNavigation, useSubmit } from "react-router-dom";

import MainNavigation from "../components/MainNavigation";
import { useEffect } from "react";

function RootLayout() {
  // const navigation = useNavigation();
  const token = useLoaderData("root");
  const submit = useSubmit();

  useEffect(() => {
    if (!token) {
      return;
    }

    setTimeout(() => {
      submit(null, { action: "/logout", method: "post" });
    }, 1 * 60 * 60 * 1000);
  }, [token, submit]);

  return (
    <>
      <MainNavigation />
      <main>
        {/* {navigation.state === 'loading' && <p>Loading...</p>} */}
        <Outlet />
      </main>
    </>
  );
}

export default RootLayout;

위 코드에서는 RootLayout 컴포넌트에서 useEffect를 사용하여 토큰의 만료 시간을 설정합니다. 코드를 자세히 살펴보면, 토큰이 존재하는 경우 setTimeout 함수를 사용하여 1시간 후에 로그아웃 요청을 전송합니다.

토큰만료 개선

토큰 만료 시간을 추가하여 사용자 인증을 더 안전하게 만드는 방법에 대해 알아보겠습니다. 이를 위해 Authentication 컴포넌트와 관련 기능을 수정하고, 만료 시간을 이용해 자동 로그아웃 기능을 구현합니다.

Authentication 컴포넌트에서 token expiration 추가

먼저, Authentication 컴포넌트에 토큰 만료 시간을 추가해봅시다.

  localStorage.setItem('token', token);
  const expiration = new Date();
  expiration.setHours(expiration.getHours() + 1);
  localStorage.setItem('expiration', expiration.toISOString());

위 코드를 사용하여 토큰과 함께 만료 시간을 저장합니다.

auth 컴포넌트 수정

auth 컴포넌트를 수정하여 expiration을 사용하도록 합니다.

import { redirect } from "react-router-dom";

export const getTokenDuration = () => {
  const storedExpirationDate = localStorage.getItem("expiration");
  const expirationDate = new Date(storedExpirationDate);
  const now = new Date();
  const duration = expirationDate.getTime() - now.getTime();
  return duration;
};

export const getAuthTokens = () => {
  const token = localStorage.getItem("token");

  if(!token) {
    return null;
  }

  const tokenDuration = getTokenDuration();

  if (tokenDuration <= 0) {
    return "EXPIRED";
  }

  return token;
};

export const tokenLoader = () => {
  return getAuthTokens();
};

export const checkAuthLoader = () => {
  const token = getAuthTokens();

  if (!token) {
    return redirect("/login");
  }

  return null;
};

위 코드에서는 만료 시간을 계산하는 getTokenDuration() 함수를 사용하여 토큰이 만료되었는지 확인합니다.

root 컴포넌트에서 useEffect 수정

root 컴포넌트의 useEffect를 수정하여 expiration을 사용하도록 합니다.

  useEffect(() => {
    if (!token) {
      return;
    }

    if(token === "EXPIRED") {
      submit(null, { action: "/logout", method: "post" });
    }

    const tokenDuration = getTokenDuration();

    setTimeout(() => {
      submit(null, { action: "/logout", method: "post" });
    }, tokenDuration);
  }, [token, submit]);

이 코드를 사용하면 토큰이 만료되면 자동으로 로그아웃되도록 설정할 수 있습니다.

logout 컴포넌트에서 expiration 제거 추가

마지막으로, 로그아웃 컴포넌트에서 expiration을 제거하도록 수정합니다.

import { redirect } from "react-router-dom";

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

이렇게 토큰 만료 시간을 추가하고 관련 기능을 수정하면 사용자 인증을 더 안전하게 만들 수 있습니다. 만료 시간을 이용해 자동으로 로그아웃되도록 설정하여 편리하게 사용할 수 있습니다.

profile
ppppqqqq

0개의 댓글