본문 바로가기
프로젝트/로그인, 회원가입

2. JWT 토큰 검증

by 갱생angel 2024. 5. 20.

※JWT를 이용한 토큰 검증 방식 사용자 인증 미들웨어

 

-백엔드-

 

middleware - <authMiddleware.js> : JWT를 사용하여 사용자의 인증을 확인하고 요청을 보호하는 미들웨어

  -split() : 문자열을 특정 기준으로 잘라서 배열로 변환하는 함수

    -req.headers.authorization.split("Bearer ")[1]; : [0] : Bearer, [1] : 실제 토큰

    (중간에 공백이 존재하기 떄문) -> ["Bearer", "<token>"]

const jwt = require("jsonwebtoken");
const User = require("../model/userModel");
const jwtSecret = process.env.JWT_SECRET;

const authUser = async (req, res, next) => {
  if (req.headers.authorization) { //요청 헤더에 토큰이 포함되어 있는지 확인
    try {
      const token = req.headers.authorization.split("Bearer ")[1]; //Bearer 문자열 뒤에 토큰을 추출
      const decoded = jwt.verify(token, jwtSecret); //토큰 검증해서 디코드
      req.user = await User.findById(decoded.id); //토큰 정보에서 사용자 ID가 포함된 document를 가져옴
      next();
    } catch (err) {
      console.log(err);
    }
  } else {
    res.status(401);
  }
};

module.exports = { authUser };

 

route - <userRoute.js> : authUser 미들웨어 적용

const express = require("express");
const router = express.Router();
const {
  loginUser,
  registerUser,
  addImageScore,
  addCombineScore,
  getUserData,
} = require("../controller/userController");
const { authUser } = require("../middleware/authMiddleware");

router.route("/login").get(authUser, getUserData).post(loginUser);
router.route("/register").post(registerUser);
router.route("/imageScore").post(authUser, addImageScore);
router.route("/combineScore").post(authUser, addCombineScore);

module.exports = router;

※사용자마다 로그인 시 각 사용자 정보 가져오기

※로그인 해야만 들어갈 수 있는 페이지와 로그인하면 들어갈 수 없는 페이지 구분

  -로그인해야 들어갈 수 있는 페이지 : 메인, 게임, 커뮤니티, 마이페이지, 학습페이지

  -로그인하면 들어갈 수 없는 페이지 : 로그인, 회원가입

※localStorage에 token을 저장

※로그아웃 기능 구현 : localStorage에 token을 삭제

 

-프론트엔드-

 

page - <Login.js> : 로그인 페이지

  -localStorage에 token을 저장

import React, { useEffect, useState } from "react";
import axios from "axios";
import "../../css/user.css";
import { useNavigate } from "react-router-dom";

function Login() {
  const navigate = useNavigate();

  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");

  const changeUsername = (e) => setUsername(e.target.value);
  const changePassword = (e) => setPassword(e.target.value);

  const loginSubmit = async (e) => {
    e.preventDefault();

    const loginData = { username: username, password: password };

    if (username === "" || password === "") {
      alert("ID, 비밀번호를 입력해주세요.");
    } else {
      try {
        await axios
          .post("http://localhost:5000/login", loginData)
          .then((res) => {
            alert(res.data.message);
            const { token } = res.data; //서버에서 토큰을 가져옴
            localStorage.setItem("token", token); //토큰은 localStorage에 저장
            navigate("/main");
          });
      } catch (err) {
        alert(err.response.data.message);
      }
    }
  };

  useEffect(() => { //로그인 상태일 시 메인페이지로 넘어감
    if (localStorage.getItem("token") !== null) navigate("/main"); 
  }, [navigate]);

  return (
    <div className="userContainer">
      <h1>로그인</h1>
      <p></p>
      <form onSubmit={loginSubmit}>
        <div>
          <label>아이디</label>
          <input type="text" value={username} onChange={changeUsername} />
        </div>
        <div>
          <label>비밀번호</label>
          <input type="password" value={password} onChange={changePassword} />
        </div>
        <button type="submit">로그인</button>
      </form>
      <p onClick={() => navigate("/register")}>-#계정 생성-</p>
    </div>
  );
}

export default Login;

 

page - <Register.js> : 회원가입 페이지

import React, { useEffect, useState } from "react";
import axios from "axios";
import "../../css/user.css";
import { useNavigate } from "react-router-dom";

function Register() {
  const navigate = useNavigate();

  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [checkPassword, setCheckPassword] = useState("");

  const changeUsername = (e) => setUsername(e.target.value);
  const changePassword = (e) => setPassword(e.target.value);
  const changeCheckPassword = (e) => setCheckPassword(e.target.value);

  const registerSubmit = async (e) => {
    e.preventDefault();

    const registerData = {
      username: username,
      password: password,
      chackPassword: checkPassword,
    };

    if (username === "" || password === "" || checkPassword === "") {
      alert("아이디, 비밀번호를 입력해주세요.");
    } else {
      try {
        await axios
          .post("http://localhost:5000/register", registerData)
          .then((res) => {
            alert(res.data.message);
            navigate("/");
          });
      } catch (err) {
        alert(err.response.data.message);
      }
    }
  };

  useEffect(() => { //로그인 상태일 시 메인페이지로 넘어감
    if (localStorage.getItem("token") !== null) navigate("/main");
  }, [navigate]);

  return (
    <div className="userContainer">
      <h1>회원가입</h1>
      <form onSubmit={registerSubmit}>
        <div>
          <label>아이디</label>
          <input type="text" value={username} onChange={changeUsername} />
        </div>
        <div>
          <label>비밀번호</label>
          <input type="password" value={password} onChange={changePassword} />
        </div>
        <div>
          <label>비밀번호 확인</label>
          <input
            type="password"
            value={checkPassword}
            onChange={changeCheckPassword}
          />
        </div>
        <button type="submit">회원가입</button>
      </form>
      <p onClick={() => navigate("/")}>-#로그인-</p>
    </div>
  );
}

export default Register;

 

page - <Home.js> : 메인 페이지

  -Bearer : 토큰을 소유한 사람에게 액세스 권한을 부여하는 일반적인 토큰 클래스로, JWT 혹은 OAuth에 대한 토큰을 인증 방식으로 한다.

  -Authorization : 권한 부여 유형

import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import axios from "axios";

function Home() {
  const navigate = useNavigate();
  const [userData, setUserData] = useState([]);

  const resetImage = () => {
    axios.post("http://localhost:5000/game/reset");
    navigate("/imageGame");
  };
  const resetText = () => {
    axios.post("http://localhost:5000/game/reset");
    navigate("/combineGame");
  };
  const logout = () => { //로그아웃 기능
    navigate("/");
    localStorage.removeItem("token"); //localStorage에 토큰을 삭제
  };

  const fetchUserData = async () => { //사용자 정보를 가져오는 함수
    const token = localStorage.getItem("token"); //localStorage에 저장된 토큰을 저장

    const headerData = { //헤더 정보
      headers: {
        Authorization: `Bearer ${token}`, //권한 부여 유형 지정
      },
    };

    try {
      await axios.get("http://localhost:5000/login", headerData).then((res) => {
        setUserData(res.data);
      });
    } catch (err) {
      console.log(err);
    }
  };

  useEffect(() => {
    if (localStorage.getItem("token") === null) {
      navigate("/");
    } else {
      fetchUserData();
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [navigate]);

  return (
    <div>
      <div>
        <button onClick={() => navigate("/")}>로그인</button>
        <button onClick={() => navigate("/register")}>회원가입</button>
        <button onClick={() => navigate("/post")}>커뮤니티</button>
        <button onClick={resetImage}>이미지 게임</button>
        <button onClick={resetText}>낱말 조합</button>
        <button onClick={logout}>로그아웃</button>
      </div>
      <div>
        <h1>유저 이름 : {userData.username}</h1>
        <h1>이미지 게임 점수 : {userData.imageScore}</h1>
        <h1>조합 게임 점수 : {userData.combineScore}</h1>
      </div>
    </div>
  );
}

export default Home;

 

page - <ImageGame.js> : 이미지 게임 페이지

import axios from "axios";
import React, { useEffect, useState } from "react";
import "../../css/game.css";
import Canvas from "../../component/Canvas";
import Typing from "../../component/Typing";
import { useNavigate } from "react-router-dom";

function ImageGame() {
  const navigate = useNavigate();

  const winNum = 1;
  const [imageData, setImagaData] = useState([]);

  const [quiz, setQuiz] = useState("");
  const [count, setCount] = useState(1);
  const [score, setScore] = useState(0);

  const [gameOver, setGameOver] = useState(false);
  const [checkQuiz, setCheckQuiz] = useState(false);
  const [moreChance, setMoreChance] = useState(0);

  const [answerObj, setAnswerObj] = useState(false);
  const [answerObjName, setAnswerObjName] = useState("타이핑");
  const [answerObjButton, setAnswerObjButton] = useState(false);

  const resetButton = () => window.location.reload();

  const toggleAnswerObj = () => {
    setAnswerObj((answerObj) => !answerObj);
    setAnswerObjName((prev) => (prev === "타이핑" ? "캔버스" : "타이핑"));
  };

  const checkAnswer = async (text) => {
    if (text === quiz) {
      alert("정답입니다.");
      setCheckQuiz(true);
      setAnswerObjButton(true);
      setScore((score) => score + 10);
    } else {
      if (moreChance === 0) {
        alert("오답입니다. 한 번 더 시도해보세요.");
        setMoreChance((moreChance) => moreChance + 1);
      } else if (moreChance === 1) {
        alert("오답입니다. 다음 라운드로 넘어갑니다.");
        setMoreChance(0);
        setCheckQuiz(true);
        setAnswerObjButton(true);
      }
    }
  };

  const fetchData = async () => {
    try {
      axios.get("http://localhost:5000/game").then((res) => {
        if (res.data.game && res.data.game.length > 0) {
          setImagaData(res.data.game[0].image);
          setQuiz(res.data.game[0].title);
          setCount(res.data.count);
          if (count >= 5) {
            setGameOver(true);
            alert(res.data.message);
          }
        } else {
          setGameOver(true);
          alert(res.data.message);
        }
      });
    } catch (err) {
      console.log(err);
    }
  };

  const updateScore = async () => {
    const token = localStorage.getItem("token"); //localStorage에 토큰을 저장

    const headerData = { //헤더 정보
      headers: {
        Authorization: `Bearer ${token}`, //권한 부여 유형 지정
      },
    };

    try {
      await axios.post(
        "http://localhost:5000/imageScore",
        { imageScore: winNum },
        headerData
      );
    } catch (error) {
      console.error(error);
    }
  };

  useEffect(() => {
    if (localStorage.getItem("token") === null) {
      navigate("/");
    } else {
      fetchData();
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [navigate]);

  useEffect(() => {
    if (score >= 50) updateScore();
  }, [score]);

  return (
    <div className="imageGameContainer">
      <div className="imageDiv">
        {gameOver ? (
          <div>
            <h1>Game Over, 점수: {score} / 50</h1>
            <button onClick={resetButton}>다시하기</button>
            <button onClick={() => navigate("/main")}>홈으로</button>
          </div>
        ) : (
          <div>
            <div className="roundDiv">
              <h2>Round: {count} / 5</h2>
              <button onClick={toggleAnswerObj} disabled={answerObjButton}>
                {answerObjName}
              </button>
            </div>
            <img alt="이미지" src={`http://localhost:5000/file/${imageData}`} />
          </div>
        )}
      </div>
      {!gameOver && (
        <div>
          {answerObj ? (
            <div>
              <Typing
                checkAnswer={checkAnswer}
                fetchData={fetchData}
                quiz={quiz}
                checkQuiz={checkQuiz}
                setCheckQuiz={setCheckQuiz}
                setAnswerObjButton={setAnswerObjButton}
              />
            </div>
          ) : (
            <div>
              <Canvas
                checkAnswer={checkAnswer}
                fetchData={fetchData}
                quiz={quiz}
                checkQuiz={checkQuiz}
                setCheckQuiz={setCheckQuiz}
                setAnswerObjButton={setAnswerObjButton}
              />
            </div>
          )}
        </div>
      )}
    </div>
  );
}

export default ImageGame;

 

page - <CombineGame.js> : 낱말 조합 게임 페이지

import axios from "axios";
import React, { useEffect, useState } from "react";
import "../../css/game.css";
import Canvas from "../../component/Canvas";
import Typing from "../../component/Typing";
import { CHO, JUNG, JONG } from "../../component/Word";
import { useNavigate } from "react-router-dom";

function CombineGame() {
  const navigate = useNavigate();

  const winNum = 1;
  const [charArray, setCharArray] = useState([]);

  const [quiz, setQuiz] = useState("");
  const [count, setCount] = useState(1);
  const [score, setScore] = useState(0);

  const [gameOver, setGameOver] = useState(false);
  const [checkQuiz, setCheckQuiz] = useState(false);
  const [moreChance, setMoreChance] = useState(0);

  const [answerObj, setAnswerObj] = useState(false);
  const [answerObjName, setAnswerObjName] = useState("타이핑");
  const [answerObjButton, setAnswerObjButton] = useState(false);

  const separateText = () => {
    const result = [];
    for (let char of quiz) {
      const unicode = char.charCodeAt(0) - 44032;

      const choIndex = parseInt(unicode / 588);
      const jungIndex = parseInt((unicode - choIndex * 588) / 28);
      const jongIndex = parseInt(unicode % 28);

      const choChar = CHO[choIndex];
      const jungChar = JUNG[jungIndex];
      const jongChar = JONG[jongIndex];

      result.push(choChar, jungChar, jongChar);
    }
    return result;
  };

  const resetButton = () => window.location.reload();

  const toggleAnswerObj = () => {
    setAnswerObj((answerObj) => !answerObj);
    setAnswerObjName((prev) => (prev === "타이핑" ? "캔버스" : "타이핑"));
  };

  const checkAnswer = (text) => {
    if (text === quiz) {
      alert("정답입니다.");
      setCheckQuiz(true);
      setAnswerObjButton(true);
      setScore((score) => score + 10);
    } else {
      if (moreChance === 0) {
        alert("오답입니다. 한 번 더 시도해보세요.");
        setMoreChance((moreChance) => moreChance + 1);
      } else if (moreChance === 1) {
        alert("오답입니다. 다음 라운드로 넘어갑니다.");
        setMoreChance(0);
        setCheckQuiz(true);
        setAnswerObjButton(true);
      }
    }
  };

  const fetchData = async () => {
    try {
      axios.get("http://localhost:5000/game").then((res) => {
        if (res.data.game && res.data.game.length > 0) {
          setQuiz(res.data.game[0].title);
          setCount(res.data.count);
          if (count >= 5) {
            setGameOver(true);
            alert(res.data.message);
          }
        } else {
          setGameOver(true);
          alert(res.data.message);
        }
      });
    } catch (err) {
      console.log(err);
    }
  };

  const updateScore = async () => {
    const token = localStorage.getItem("token"); //localStorage의 토큰 저장

    const headerData = { //헤더 정보
      headers: {
        Authorization: `Bearer ${token}`, //권한 부여 유형 지정
      },
    };

    try {
      await axios.post(
        "http://localhost:5000/combineScore",
        { combineScore: winNum },
        headerData
      );
    } catch (error) {
      console.error(error);
    }
  };

  useEffect(() => {
    if (localStorage.getItem("token") === null) {
      navigate("/");
    } else {
      fetchData();
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [navigate]);

  useEffect(() => {
    setCharArray(separateText().sort(() => Math.random() - 0.5));
    //eslint-disable-next-line react-hooks/exhaustive-deps
  }, [quiz]);

  useEffect(() => {
    if (score >= 50) updateScore();
  }, [score]);

  return (
    <div className="combineGameContainer">
      <div className="combineDiv">
        {gameOver ? (
          <div>
            <h1>Game Over, 점수: {score} / 50</h1>
            <button onClick={resetButton}>다시하기</button>
            <button onClick={() => navigate("/main")}>홈으로</button>
          </div>
        ) : (
          <div>
            <div className="roundDiv">
              <h2>Round: {count} / 5</h2>
              <button onClick={toggleAnswerObj} disabled={answerObjButton}>
                {answerObjName}
              </button>
            </div>
            <div className="textQuizDiv">
              {charArray.map((char, index) => (
                <span key={index}>{char}</span>
              ))}
            </div>
          </div>
        )}
      </div>
      {!gameOver && (
        <div>
          {answerObj ? (
            <div>
              <Typing
                checkAnswer={checkAnswer}
                fetchData={fetchData}
                quiz={quiz}
                checkQuiz={checkQuiz}
                setCheckQuiz={setCheckQuiz}
                setAnswerObjButton={setAnswerObjButton}
              />
            </div>
          ) : (
            <div>
              <Canvas
                checkAnswer={checkAnswer}
                fetchData={fetchData}
                quiz={quiz}
                checkQuiz={checkQuiz}
                setCheckQuiz={setCheckQuiz}
                setAnswerObjButton={setAnswerObjButton}
              />
            </div>
          )}
        </div>
      )}
    </div>
  );
}

export default CombineGame;

 

<App.js>

import React from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import Login from "./page/User/Login";
import Register from "./page/User/Register";
import PostAdd from "./page/Comunity/PostAdd";
import PostList from "./page/Comunity/PostList";
import PostDetail from "./page/Comunity/PostDetail";
import PostUpdate from "./page/Comunity/PostUpdate";
import Home from "./page/Home";
import ImageRegist from "./page/Game/ImageRegist";
import ImageGame from "./page/Game/ImageGame";
import CombineGame from "./page/Game/CombineGame";

function App() {
  return (
    <div>
      <BrowserRouter>
        <Routes>
          <Route index element={<Login />} />
          <Route path="/main" element={<Home />} />
          <Route path="/register" element={<Register />} />
          <Route path="/post" element={<PostList />} />
          <Route path="/post/add" element={<PostAdd />} />
          <Route path="/post/:id" element={<PostDetail />} />
          <Route path="/post/:id/update" element={<PostUpdate />} />
          <Route path="/image/add" element={<ImageRegist />} />
          <Route path="/imageGame" element={<ImageGame />} />
          <Route path="/combineGame" element={<CombineGame />} />
        </Routes>
      </BrowserRouter>
    </div>
  );
}

export default App;

 

'프로젝트 > 로그인, 회원가입' 카테고리의 다른 글

6. 비밀번호 변경  (0) 2024.05.27
5. nodemailer, 인증코드  (0) 2024.05.27
4. 아이디 찾기  (0) 2024.05.24
3. 이메일, 정규 표현식  (0) 2024.05.23
1. user 파일, score 파일 병합  (0) 2024.05.20