본문 바로가기
React

[React] router v6에서 JWT 인증과 Private 컴포넌트를 통한 화면 접근 보안 구현하기

by 흑시바 2023. 2. 25.

토이프로젝트 진행 중 서버에서 JWT 인증을 구축한 뒤, 로그인한 경우에만(클라이언트가 정상적인 access_token을 가진 경우에만) 특정 화면에 접근할 수 있도록 하려면 어떻게 해야 할지 고민하게 되었다. 😿

 

필자의 경우 별도의 Private 컴포넌트를 생성해서 접근하는 순간 자동으로 서버에 access 토큰 인증 요청을 하게 되고, 인증에 실패한 경우는 클라이언트가 해당 화면에 접근하지 못하도록 구현했다.

 

방식은 간단하다. Route 컴포넌트 element에 실제 접근하려는 컴포넌트 대신 Private 컴포넌트를 전달하고,

Private에 컴포넌트에 실제 접근하려는 컴포넌트를 속성으로 넘기면 된다.

[TO-BE]

const App = () => {
  return (
    <Routes>
      <Route path="/" element={<HomePage />} />
      <Route path="/auth/login" element={<LoginPage />} />
      <Route path="/member" element={<MemberListPage />} />
    </Routes>
  );
};

 

기존에는 따로 토큰을 검증하는 일이 없었기 때문에, /member에 자유롭게 접근이 가능했다.

[AS-IS] 1. 한 가지 컴포넌트에만 적용하기

const App = () => {
  return (
    <Routes>
      <Route path="/" element={<HomePage />} />
      <Route path="/auth/login" element={<LoginPage />} />
      <Route path="/member" element={<Private component={<MemberListPage />}/>}/>
    </Routes>
  );
};

 

한 가지 컴포넌트에만 적용하려고 한다면 Private 컴포넌트를 대신 전달하고, 실제 접근하려는 컴포넌트를 전달하면 인증 후 접근하도록 할 수 있다.

[Private]

import React from 'react';
import { useState } from 'react';
import { useEffect } from 'react';
import { Navigate } from 'react-router-dom';
import { isAuth } from '../lib/api';
import { setCookieToken } from './Cookie';

const Private = ({ component }) => {
  const [loggedIn, setLoggedIn] = useState(undefined);

  const checkAuth = async () => {
    try {
      const response = await isAuth();
      setLoggedIn(true);
      // access_token이 만료됐다면, refresh token으로 갱신한다.
      if (response.data !== '') {
        setCookieToken(
          response.data.accessToken,
          response.data.refreshToken,
          response.data.expiredTime,
        );
      }
    } catch (e) {
      alert('해당 화면에 접근할 권한이 없습니다!');
      setLoggedIn(false);
    }
  };

  useEffect(() => {
    checkAuth();
  }, []);

  const showComponent = () => {
    if (loggedIn !== undefined) {
      if (loggedIn) {
        return component;
      }
      return <Navigate to="/auth/login" />;
    }
  };

  return showComponent();
};

export default Private;

 

[AS-IS] 2. 여러 컴포넌트에 동시에 적용하기

 

const App = () => {
  return (
    <Routes>
      <Route element={<Private />}>
        <Route path="/member1" element={<MemberListPage1 />}/>
        <Route path="/member2" element={<MemberListPage2 />}/>
        <Route path="/member3" element={<MemberListPage2 />}/>
      </Route>
      <Route path="/" element={<HomePage />} />
      <Route path="/auth/login" element={<LoginPage />} />
    </Routes>
  );
};

 

여러 컴포넌트에 적용하려고 한다면 중첩 라우팅을 활용하면 깔끔하게 정리할 수 있다.

Private 코드에서 component가 아닌 <Outlet />으로 전달해 주도록 수정하면 된다.

[Private]

import React from 'react';
import { useState } from 'react';
import { useEffect } from 'react';
import { Navigate, Outlet } from 'react-router-dom';
import { isAuth } from '../lib/api';
import { setCookieToken } from './Cookie';

const Private = () => {
  const [loggedIn, setLoggedIn] = useState(undefined);

  const checkAuth = async () => {
    try {
      const response = await isAuth();
      setLoggedIn(true);
      // access_token이 만료됐다면, refresh token으로 갱신한다.
      if (response.data !== '') {
        setCookieToken(
          response.data.accessToken,
          response.data.refreshToken,
          response.data.expiredTime,
        );
      }
    } catch (e) {
      alert('해당 화면에 접근할 권한이 없습니다!');
      setLoggedIn(false);
    }
  };

  useEffect(() => {
    checkAuth();
  }, []);

  const showComponent = () => {
    if (loggedIn !== undefined) {
      if (loggedIn) {
        return <Outlet/>;
      }
      return <Navigate to="/auth/login" />;
    }
  };

  return showComponent();
};

export default Private;

Private 컴포넌트의 프로세스는 다음과 같다.

1. Private 컴포넌트에 접근하면서 useEffect()를 통해 checkAuth() 메서드가 실행된다.

2. checkAuth() 메서드는 isAuth() 메서드를 호출하여 서버에 access_token 검증을 요청한다.

3. 서버에서 검증했을 때, 올바른 access_token 및 refresh_token을 보유한 경우,

- loggedIn을 true 값으로 변경해서 인증에 성공했다는 처리를 한다.

- 만약 refresh_token을 통해 토큰 값이 변경된 경우, 쿠키 값을 변경한다.

4. 서버에서 검증했을 때, 올바르지 못한 access_token 및 refresh_token을 보유한 경우,

- loggedIn을 false 값으로 변경해서 인증에 실패했다는 처리를 한다.

5. showComponent()를 반환한다.

6. loggedIn이 undefined가 아닌지 확인한다. (인증 처리가 끝났는지 확인)

7. 인증처리가 끝난 경우, 인증이 성공했다면 실제 접근하려 했던 컴포넌트를 반환한다.

8. 인증에 실패했다면, 로그인 화면으로 이동한다.

 

 

그런데 포스팅을 보면 토큰은 어디서 어떻게 넘기는 건지(?) 궁금할 수 있다.

 

요약하자면..

1. 로그인에 성공하면 Cookie에 access_token과 refresh_token을 저장한다.

2. 서버 Controller에서 HttpServletRequest 파라미터를 활용해 Cookie 데이터에 접근한다.

3. 이를 통해 access_token과 refresh_token에 접근하여 인증 처리를 한다.

 

이와 관련된 백엔드 관련 처리 내용은 추후 포스팅 예정이다.

 

REFERENCE

https://dev.to/iamandrewluca/private-route-in-react-router-v6-lg5

 

🔐 Private Route in react-router v6

Things are changing fast in WEB today, and react-router v6 is in beta already and around the corner....

dev.to

https://reactrouter.com/en/main/components/outlet

 

Outlet v6.9.0

Type declarationinterface OutletProps { context?: unknown; } declare function Outlet( props: OutletProps ): React.ReactElement | null; An should be used in parent route elements to render their child route elements. This allows nested UI to show up when ch

reactrouter.com

 

댓글