리액트앱 인증 추가하기

맛없는콩두유·2022년 9월 23일
2

소개

웹앱에서는 인증 DOM이 필요합니다. 로그인해야만 하는 접근 구역이 있습니다.

바로 리액트 앱에서의 인증에 대해서 아랑보겠습니다.

가입, 로그인, 로그아웃을 구현하고 사용자 로그인 시 보호된 리소스에 접근하는 법을 알아보고, 로그인을 유지해주는 인증유지와 그리고 일정시간이 지난 후 자동으로 로그아웃에 관련된 것들도 알아보겠습니다!

무엇을 어떻게 왜?

인증의 필요성 부터 일해해 봅시다!
바로 보호해야 할 정보가 있기 떄문입니다.

일반적으로 인증은 2단계 절차를 거칩니다.

1단계는 사용자가 접근 허가를 받는 겁니다.

그러려면 계정부터 만들어야겠죠!

인증은 예, 아니오 보다 더 확실해야합니다.
그럴 때 쓰는 기법은 크게 2가지입니다.
서버 사이드 세션을 활용하거나, 인증 토큰을 활용해요.

  • 서버 사이드 섹션

서버 사이드 섹션은 특정 사용자의 고유 ID를 저장합니다.
즉, 모든 사이트 방문자의 고유 ID가 서버에 저장되죠. 클라이언트에게도 전송됩니다.
ID를 위조할 수 없죠 임의로 만든 ID는 서버가 못 알아보니 접근이 거부되고 후속 요청도 불가합니다. 훌륭한 방식입니다! 하지만 단점이 하나 있습니다. 싱글 페이지 앱은 결합이 느슨해서 백엔드와 프론트엔드 분리 상태는 앱 개발에서 자주 맞닥뜨릴 것이기 떄문에 중점적으로 다뤄보겠습니다!

  • 인증 토큰

사용자가 이메일과 비밀번호로 서버에 증명을 보내면 서버가 그걸 데이터베이스에서 비교해 확인하는 것까지는 같습니다. 하지만 자격이 증면되면 서버가 허가 토큰이라는 걸 생성합니다.
아주 긴 문자열이에요! 서버가 특정 알고리즘을 사용해 이메일 주소를 비롯한 데이터를 한 문자열로 인코딩하면 나중에 다시 개별 데이터로 디코딩하는 것 입니다. 이떄 중요한 건 서버만 아는 키를 사용해 데이터를 문자열로 해싱한다는 겁니다. 클라이언트는 모르고 서버만 압니다.

설정 시작 및 첫번쨰 단계

이번 강좌 모듈엑서도 파이어 베스를 쓸 겁니다! 이번엔 인증에 쓸 겁니다.

firebase auth rest api를 검색하면 앞으로 사용할 Docs에 관한 것을 볼 수 있습니다!

먼저 Firebase를 가입하고 Build에서 Authentication을 사용할 겁니다!

사용자 가입 추가하기

인증을 추가해봅시다!
먼저 파이어베이스에서 가짜 백엔드로 쓰겠습니다
파이어베이스 인증 REST API를 사용하겠습니다!

  • AuthForm.js

    로그인 모드에서 SUbmit 눌렀을 떄 보일 반응을 설정하겠습니다.

    사용자가 입력한 값을 받아서 로그인/회원가입 모드인 지 확인할 겁니다.

import { useState, useRef } from "react";


const AuthForm = () => {
  const emailInputRef = useRef();
  const passwordInputRef = useRef();

  const submitHandler = (e) => {
    e.preventDefault();

    const enteredEmaili = emailInputRef.current.value;
    const enteredPassword = passwordInputRef.current.value;

    if (isLogin) {
    } else {
      fetch(
        "https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=AIzaSyBDYUE4kI-pR-n26ixFR43IfzQvtwtj-p0",
        {
          method: "POST",
          body: JSON.stringify({
            email: enteredEmaili,
            password: enteredPassword,
            returnSecureToken: true,
          }),
          headers: {
            "Content-Type": "application/json",
          },
        }
      ).then((res) => {
        if (res.ok) {
          // ...
        } else {
          return res.json().then((data) => {
            // show an error modal
            console.log(data);
          });
        }
      });
    }
    
    ...
    
    <form onSubmit={submitHandler}>
    ...
    
    </fome>

useRef를 활용해서 현재 입력된 값을 얻어올 수 있습니다. 이메일과 비밀번호를 useRef()로 설정하고 submitHandler 메서드를 만들고 event 발생시 기본 성질을 막고 현재 이메일과 비밀번호의 입력을 받아서 로그인 되어있을 떄와 안되어있을 떄를 구분해 안되어있을 떄는 fetch를 이용하여 POST 방식으로 email과 password reutnrSecureToken을 보내고 .then을 통해서 비동기 통신을하여 오류를 잡아내는 것도 볼 수 있습니다.

비밀번호를 4글자만 입력하면 에러가 뜨는데 이것은 FireBase에서 기본적으로 제공하는 예외처리이다. 6글자를 입력하면
입력한 이메일 비밀번호가 FireBase에 저장된 걸 볼 수 있다.

사용자에게 피드백 표시하기

  • AuthForm.js

alert창으로 사용자에게 가입 실패 시 alert 창으로 사용자에게 메시지를 보내겠습니다.

if (res.ok) {
        } else {
          return res.json().then((data) => {
            let errorMessage = "Authentication failed!";
            // if (data && data.error && data.error.message) {
            //   errorMessage = data.error.message;
            // }
            alert(errorMessage);
          });

를 추가하고 로딩을 추가하되 로딩 중이 아닐 떄만 Create Account가 보이게 하겠습니다!

일단, State가 필요하여

const [isLoading, setIsLoading] = useState(false);


 setIsLoading(true);
    if (isLogin) {
    } else {
      fetch(
        "https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=AIzaSyBDYUE4kI-pR-n26ixFR43IfzQvtwtj-p0",
        {
          method: "POST",
          
   ....
   
   then((res) => {
        setIsLoading(false);
        if (res.ok) {
        } else {

로딩을 응다받기 전에 true로 설정하고
응답을 받으면 다시 false로 설정합니다.

{!isLoading && (
            <button>{isLogin ? "Login" : "Create Account"}</button>
          )}
          {isLoading && <p>Sending request....</p>}

그리고 버튼이 isLoading이 아닐 떄만 보이게 하고 isLoading일 때는 Sending request.... 메시지가 보이게 합니다!

코드를 입력하세요

Message가 잘 보이는 것을 볼 수 있습니다!

사용자 로그인 추가하기

로그인을 하려면
https://firebase.google.com/docs/reference/rest/auth#section-sign-in-email-password
에서 https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=[API_KEY] 를 입력하라고 되어있습니다.

  • AuthForm.js
let url;
    if (isLogin) {
      url =
        "https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=AIzaSyBDYUE4kI-pR-n26ixFR43IfzQvtwtj-p0";
    } else {
      url =
        "https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=AIzaSyBDYUE4kI-pR-n26ixFR43IfzQvtwtj-p0";
    }
    fetch(url, {
      method: "POST",
      body: JSON.stringify({

url을 let으로 설정해주고 if(isLoagin)시에 url을 적어주고 key 값을 설정해줍니다.

else{
 return res.json().then((data) => {
            let errorMessage = "Authentication failed!";

            throw new Error(errorMessage);

res가 실패했을 때 return으로 throw new Error(errorMessage)를 설정해주고

  .then((data) => {
        console.log(data);
      })
      .catch((err) => {
        alert(err.message);
      });

해주면


유효하게 회원가입이 적용되고, 로그인을 하면 콘솔창에 이런 문구가 뜨게 됩니다.

컨텍스트로 인증 State 관리하기

모든 컴포넌트에서 사용할 수 있도록 이 토큰을 저장해야합니다!

앱 와이드 상태를 관리할 땐 컨텍스트 API와 리덕스가 있습니다.
컨텍스트 API를 사용하겠습니다!

  • autu-context.js
import React, { useState } from "react";

const AuthContext = React.createContext({
  token: "",
  isLoggedIn: false,
  login: (token) => {},
  logout: () => {},
});

export const AuthContextProvider = (props) => {
  const [token, setToken] = useState(null);

  const userIsLoggedIn = !!token; // token이 유효하면 true 아니면 false로 바꿔줌!

  const loginHandler = (token) => {
    setToken(token);
  };
  const logoutHandler = () => {
    setToken(null);
  };

  const contextValue = {
    token: token,
    isLoggedIn: userIsLoggedIn,
    login: loginHandler,
    logout: logoutHandler,
  };

  return (
    <AuthContext.Provider value={contextValue}>
      {props.children}
    </AuthContext.Provider>
  );
};

export default AuthContext;
  • index.js
<AuthContextProvider>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </AuthContextProvider>
  • AuthForm.js

import { useContext } from "react";
import AuthContext from "../../store/auth-context";


  const authCtx = useContext(AuthContext);
  
  .then((data) => {
        authCtx.login(data.idToken);
      })

토큰을 받는 곳이기 때문에 AutoForm에서 컨텍스트가 필요합니다!

이어서 인증 상태를 UI에 반영하는 작업을
Layout 폴더의 MainNavigation.js에서 해보겠습니다!

  • MainNavigation.js
import { useContext } from "react";

import AuthContext from "../../store/auth-context";

  const authCtx = useContext(AuthContext);
  
    const isLoggedIn = authCtx.isLoggedIn;

{!isLoggedIn && (
            <li>
              <Link to="/auth">Login</Link>
            </li>
          )}
          {isLoggedIn && (
            <li>
              <Link to="/profile">Profile</Link>
            </li>
          )}
          {isLoggedIn && (
            <li>
              <button>Logout</button>
            </li>
          )}

네비게이션에서 로그인 여부에 따라서 다르게 보여야하므로 useContext가 필요하고 IsLoggedIn을 이용하여 Login여부에 따라서 네비게이션의 Link들을 다르게 표시했다!

  • 로그인 하지 않았을 떄
  • Login 성공 시

보호된 리소스에 대한 요청에 토큰 사용하기

보호된 리소스에 요청을 보낼 경우 인증 토큰이 어떻게 작용하는지 알아봅시다!

인증 REST API를 참고하도록 하겠습니다!

사용자 입력을 얻고 요청을 보내겠습니다.

  • ProfileForm.js
import { useRef, useContext } from "react";

  const newPasswordInputRef = useRef();
  const authCtx = useContext(AuthContext);
  
  const submitHandler = (e) => {
    e.preventDefault();

    const enteredNewPassword = newPasswordInputRef.current.value;

    fetch(
      "https://identitytoolkit.googleapis.com/v1/accounts:update?key=AIzaSyBDYUE4kI-pR-n26ixFR43IfzQvtwtj-p0",
      {
        method: "POST",
        body: JSON.stringify({
          idToken: authCtx.token,
          password: enteredNewPassword,
          returnSecureToken: false,
        }),
        headers: {
          "Content-Type": "application/json",
        },
      }
    ).then((res) => {
      //Always succeeds!
    });
  };
  
  <form className={classes.form} onSubmit={submitHandler}>
  
  ...
  
   <input
          type="password"
          id="new-password"
          minLength="7"
          ref={newPasswordInputRef}
        />

7자 이상 비밀번호 변경을 누르면 변경이 됩니다!

사용자 리디렉션

비밀번호 변경이나 로그인 됐을 떄 사용자를 리디렉션 해보겠습니다! 그러고 나서 로그아웃 기능도 구현해 보겠습니다!

리액트 라우터를 통해 리디렉션할 수 있는데, History Hook을 이용하면 됩니다!

로그인 했을 때

  • AuthForm.js
import { useHistory } from "react-router-dom";

  const history = useHistory();
  
  ...
  
  .then((data) => {
        authCtx.login(data.idToken);
        history.replace("/");
      })
  • ProfileFome.js
import { useHistory } from "react-router-dom";
...

  const history = useHistory();
  
  ...
  
  .then((res) => {
      //Always succeeds!
      history.replace("/");
    });

로그인 성공/ 비밀번호 변경 시

화면으로 리디렉션 되는 것을 볼 수 있다!

로그아웃 추가하기

컨텍스트 API에서 토큰을 비우면 로그아웃이 됩니다!

  • auth-context.js
  const logoutHandler = () => {
    setToken(null);
  };
  
    const contextValue = {
    token: token,
    isLoggedIn: userIsLoggedIn,
    login: loginHandler,
    logout: logoutHandler,
  };

에서 logout 설정했으니 MainNavigation에서는 logout을 호출하기만 하면 된다!

  • MainNavigation.js
  const logoutHandler = () => {
    authCtx.logout();
  };
  
  ...
  
            {isLoggedIn && (
            <li>
              <button onClick={logoutHandler}>Logout</button>
            </li>
          )}


Logout을 누르면

Login으로 바뀝니다!

프론트 페이지 보호하기

로그인 상태가 아닐 때도 URL에 직접 /profile을 붙여 접속하면 접속이되는 문제가 발생합니다.

바로 내비게이션 가드를 추가하면 됩니다!

App.js에서 라우트 설정을 동적으로 하면 됩니다!

  • App.js
import { useContext } from "react";
import { Redirect } from "react-router-dom";


  const authCtx = useContext(AuthContext);
  
       <Route path="/profile">
          {authCtx.isLoggedIn && <UserProfile />}
          {!authCtx.isLoggedIn && <Redirect to="/auth" />}
        </Route>

        <Route path="*">
          <Redirect to="/" />
        </Route>

/profile로 가는 Route를 로그인 되었을 떄와 로그인 안되었을 때 각각 보여주고

Route path='*' 위의 Router 외 모든 경로는 /로 리디렉션 합니다!

http://localhost:3000/profile
profile을 적게되면

http://localhost:3000/auth
page로 오게되는 것을 알 수 있습니다.

http://localhost:3000/abcd
/뒤 렌더링 되는 주소외에 url을 적게되면

설정한 리디렉션 주소로 오게됩니다!

사용자 인증 state 유지하기

현재 다시 로드하거나 URL을 입력할 때마다 인증 상태를 잃습니다.

적어도 한 시간 로그인 상태였으면 좋겠습니다.

토큰을 리액트 상태 바깥 어딘가에 저장해야됩니다.
바로 브라우저에 있습니다! 쿠키입니다.

  • auth-context.js
 const initialToken = localStorage.getItem("token");
 const [token, setToken] = useState(initialToken);

우선 token이 localStorage에 있는지 확인하고 초기 값을 initialToken으로 설정합니다.

 const loginHandler = (token) => {
    setToken(token);
    localStorage.setItem("token", token);
  };

로그인 되었을 떄 네트워크 저장소에 token을 key-value 값으로 저장하고

  const logoutHandler = () => {
    setToken(null);
    localStorage.removeItem("token");
  };

로그아웃 했을 떄 token을 지워줍니다!

새로고침을 해도 Logout이 계속 보이는 것을 확인할 수 있습니다.

자동 로그아웃 추가하기

  • auth-context.js

const calculateRemainingTime = (expirationTime) => {
  const currentTime = new Date().getTime();
  const adjExpirationTime = new Date(expirationTime).getTime();

  const remainingDuration = adjExpirationTime - currentTime;

  return remainingDuration;
};



...


const loginHandler = (token, expirationTime) => {
    setToken(token);
    localStorage.setItem("token", token);

    const remainingTime = calculateRemainingTime(expirationTime);

    setTimeout(logoutHandler, remainingTime);
  };
  • AuthForm.js
then((data) => {
        const expirationTime = new Date(
          new Date().getTime() + +data.expiresIn * 1000
        );
        authCtx.login(data.idToken, expirationTime.toISOString());
        history.replace("/");
      })

마무리 단계

profile
하루하루 기록하기!

0개의 댓글