※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 |