Cookie를 구현하기 위한 Client와 Server의 흐름을 확인하고 구현한 의사코드를 작성해보며 복습해보려고 한다.
[server_index.js / 흐름0: cors설정해주기, endpoint설정해주기]
const express = require('express');
const cors = require('cors');
const logger = require('morgan');
const cookieParser = require('cookie-parser');
const fs = require('fs');
const https = require('https');
const controllers = require('./controllers');
const app = express();
//mkcert에서 발급한 인증서를 사용하기 위한 코드입니다. 삭제하지 마세요!
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
const HTTPS_PORT = process.env.HTTPS_PORT || 4000;
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
//서버는 4000번, 클라이언트는 3000번을 사용하기에 포트번호가 다르기에 크로스 오리진이기에 cors설정을 해줘야 요청과 응답이 왔다갔다함
//흐름 0: cors 설정해주기
const corsOptions = {
origin: "http://localhost:3000",
credentials: true,
//*cors요청은 option으로 주고 받음 따라서 option은 항상 넣어주기
methods: ['GET', 'POST', 'OPTION']
};
app.use(cors(corsOptions));
//로컬 호스트 4000번대의 endpoint처리를 해주고 있음
//요청에 맞게 실행시키는데 예를들어 login이라면 controllers의 login에 있는 export된 함수를 실행
app.post('/login', controllers.login);
app.post('/logout', controllers.logout);
app.get('/userinfo', controllers.userInfo);
//endpoin에 cookie라고 했을때 Hello Cookie!가 나오는지 확인해보기
app.get('/cookie', (req, res) => res.send('Hello Cookie!'));
[client_Login.js / 흐름1: 로그인 버튼을 눌렀을때 서버로 로그인 요청하기]
import React, { useState } from 'react';
import axios from 'axios';
export default function Login( {setIsLogin, setUserInfo}) {
//흐름1: 로그인을 위한 정보를 관리하는 상태관리로서 서버에 전달해줄 상태
const [loginInfo, setLoginInfo] = useState({
userId: '',
password: '',
});
const [checkedKeepLogin, setCheckedKeepLogin] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const handleInputValue = (key) => (e) => {
setLoginInfo({ ...loginInfo, [key]: e.target.value });
};
//흐름1: login button을 눌렀을때 이벤트함수가 실행되는것
const loginRequestHandler = () => {
// TODO: Login 컴포넌트가 가지고 있는 state를 이용해 로그인을 구현합니다.
// 로그인에 필요한 유저정보가 충분히 제공되지 않았다면 에러메시지가 나타나도록 구현하세요.
//axios로 login을 담당하는 endpoint로 호스트요청 보내기
//post의 두번째 인자로 필요한 데이터를 전달할 수 있음
return axios
.post("http://localhost:4000/login", {loginInfo, checkedKeepLogin})
.then((res) => {
// 로그인에 성공했다면 응답으로 받은 데이터가 Mypage에 렌더링되도록 State를 변경하세요.
//흐름3: 로그인 성공에 따라 props로 받아온 상태 변경해주기
//흐름 2에서 보내준 userInfo가 password없이 잘 들어오는지 확인
console.log(res.data)
setUserInfo(res.data) //userInfo에는 받아온 값 넣어주기
setIsLogin(true) //true로 바꿔주기
})
.catch((err) => {
// 로그인에 실패했다면 그에 대한 에러 핸들링을 구현하세요.
});
};
return (
<div className='container'>
<div className='left-box'>
<span>
Education
<p>for the</p>
Real World
</span>
</div>
<div className='right-box'>
<h1>AUTH STATES</h1>
<form onSubmit={(e) => e.preventDefault()}>
<div className='input-field'>
<span>ID</span>
<input type='text' data-testid='id-input' onChange={handleInputValue('userId')} />
<span>Password</span>
<input
type='password'
data-testid='password-input'
onChange={handleInputValue('password')}
/>
<label className='checkbox-container'>
<input type='checkbox' onChange={() => setCheckedKeepLogin(!checkedKeepLogin)} />
{' 로그인 상태 유지하기'}
</label>
</div>
<button type='submit' onClick={loginRequestHandler}>
LOGIN
</button>
{errorMessage ? (
<div id='alert-message' data-testid='alert-message'>
{errorMessage}
</div>
) : (
''
)}
</form>
</div>
</div>
);
}
[server_login.js / 흐름2: 요청받은 정보가 일치한다면 유저정보와 쿠키를 Server_userinfo.js로 전달]
const { USER_DATA } = require('../../db/data');
//흐름 2
module.exports = (req, res) => {
//client에서 보내준 정보를 잘 받아오고있는지 확인하기
console.log(req.body)
//login에 있어서 받아오고 있는 정보를 확인해보면 req.body를 통해 userId, password, checkedKeepLogin여부를 받아오고 있음
const { userId, password } = req.body.loginInfo;
const { checkedKeepLogin } = req.body;
//더미데이터로 관리하고있는 userdata를 filter해서 요청에서 받아온 userId와 password가 일치하는지 걸러내서 복사해서 저장하기
//더미데이터의 원본을 오염시킬 수 있기에 복사해서 사용하기
const userInfo = {...USER_DATA.filter((user) => user.userId === userId && user.password === password)[0]};
//콘솔로 확인해볼 수 있음.
console.log(userInfo)
//filter했을때 만약 userInfo.id가 없다면 401 상태의 권한없음을 보여주고 잘들어왔다면 쿠키를 전달해주기
const cookieOptions = {
//어떤 도메인에 저장할것인지 정해주기
domain: 'localhost',
path: '/',
//https 프로토콜을 사용할때만 쿠키를 전달해준다는 옵션(localhost는 개발용이기에 안적어주는 경우도 많음, 탈취의 위험이 없기때문, 사용하게된다면 항상 true로 하기)
secure: true,
//쿠키는 consloe창에서 확인을 할 수 있는데 httpOnly 옵션을 true로 주면 console에서 쿠키 확인을 막을 수 있음 보안을 위해 항상 true로 주기
httpOnly: true,
//프로토콜과 포트가 가르고 사이트가 같은곳만 전달하는 옵션
sameSite: 'strict'
//expir, maxAge 설정이 없기때문에 브라우저를 닫으면 쿠키가 없어짐
}
if(userInfo.id === undefined) {
res.status(401).send('Not Authorized')
}
//옵션 설정할때 expire, maxAge 설정이 없기때문에 브라우저를 닫으면 쿠키가 없어짐 따라서 로그인상태 유지 체크가 되었다면 그때 설정해주기
else if(checkedKeepLogin) {
cookieOptions.maxAge = 1000 * 60 * 30 //단위가 ms 이기때문에 1초, 1분, 30분을 이렇게 작성함
res.cookie('cookieId', userInfo.id, cookieOptions)
res.redirect('/userinfo')
}
else {
//res.cookie로 전달할 수 있고 인자로 (저장할이름, 어떤값을 저장, 저장할때 옵션) 저장해야함
res.cookie('cookieId', userInfo.id, cookieOptions)
//send로 마무리 해줘야하지만 지금은 /useinfo로 리다이렉트 해줘야함
res.redirect('/userinfo')
//개발자도구 -> 애플리케이션 -> 쿠키에 저장되는것을 확인할 수 있음
}
};
loginInfo, checkedKeepLogin 받아오고 있는지 콘솔에 확인
들어온 loginInfo 정보와 일치하는 userInfo가 있는지 콘솔에서 확인하기
정상적으로 들어왔다면 쿠키가 브라우저에 정상적으로 전송
[Server.js_userinfo.js / 흐름2: 로그인 정보에 맞는 쿠키를 전달받아 userInfo 전달하기]
const { USER_DATA } = require('../../db/data');
//흐름 2
module.exports = (req, res) => {
//cookie가 잘들어오고있는지 확인해보기 확인된 쿠키로 유저를 찾아서 보내주기
console.log(req.cookies)
//USER_DATA에 filter를 사용해서 user.id와 받아온 쿠키의 cookieId가 같은거를 가져오기
const userInfo = {...USER_DATA.filter((user) => user.id === req.cookies.cookieId)[0]};
console.log(userInfo)
//쿠키가 들어와서 회원 정보를 찾으러 갔는데 회원 정보가 없을때(회원탈퇴 등등) 로그인처리 시켜주면 안되고, 쿠키값이 제대로 들어오지 않았을때도 로그인되면 안됨
if(!req.cookies.cookieId || !userInfo.id) {
res.status(401).send('Not Authorized')
}
// 그렇지 않다면 로그인 할 수 있는데 그때 userInfo 중 민감한 정보는 삭제해야함
else {
delete userInfo.password
res.send(userInfo)
}
};
[client_App.js / 흐름3: 로그인에 맞는 상태 전달, 흐름4: 로그인 조건에 따른 조건부 렌더링]
import './App.css';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Login from './pages/Login';
import Mypage from './pages/Mypage';
import React, { useEffect, useState } from 'react';
import axios from 'axios';
// 모든 요청에 withCredentials가 true로 설정됩니다.
axios.defaults.withCredentials = true;
function App() {
//흐름3: 받아온 userInfo에 맞게 상태를 변경해줘야하기에 porps로 내려주기
const [isLogin, setIsLogin] = useState(false);
const [userInfo, setUserInfo] = useState(null);
return (
<BrowserRouter>
<div className='main'>
<Routes>
<Route
path='/'
element={
//흐름4: 로그인이 된다면 Mypage를 로그인이 안된다면 Login페이지를
isLogin ? (
//흐름3: 로그인으로 컴포넌트에서 변경할 수 있도록 props를 전달
<Mypage userInfo={userInfo}
//흐름7: 로그아웃으로 컴포넌트에서 변경할 수 있도록 props를 전달
setIsLogin={setIsLogin} setUserInfo={setUserInfo}/>
) : ( //흐름3: 로그인으로 컴포넌트에서 변경할 수 있도록 props를 전달
<Login setIsLogin={setIsLogin} setUserInfo={setUserInfo}/>
)
}
/>
</Routes>
</div>
</BrowserRouter>
);
}
export default App;
[Client_Login.js / 흐름3: 로그인 응답 받아 상태 업데이트]
import React, { useState } from 'react';
import axios from 'axios';
export default function Login( {setIsLogin, setUserInfo}) {
//흐름1: 로그인을 위한 정보를 관리하는 상태관리로서 서버에 전달해줄 상태
const [loginInfo, setLoginInfo] = useState({
userId: '',
password: '',
});
const [checkedKeepLogin, setCheckedKeepLogin] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const handleInputValue = (key) => (e) => {
setLoginInfo({ ...loginInfo, [key]: e.target.value });
};
//흐름1: login button을 눌렀을때 이벤트함수가 실행되는것
const loginRequestHandler = () => {
// TODO: Login 컴포넌트가 가지고 있는 state를 이용해 로그인을 구현합니다.
// 로그인에 필요한 유저정보가 충분히 제공되지 않았다면 에러메시지가 나타나도록 구현하세요.
//axios로 login을 담당하는 endpoint로 호스트요청 보내기
//post의 두번째 인자로 필요한 데이터를 전달할 수 있음
return axios
.post("http://localhost:4000/login", {loginInfo, checkedKeepLogin})
.then((res) => {
// 로그인에 성공했다면 응답으로 받은 데이터가 Mypage에 렌더링되도록 State를 변경하세요.
//흐름3: 로그인 성공에 따라 props로 받아온 상태 변경해주기
//흐름 2에서 보내준 userInfo가 password없이 잘 들어오는지 확인
console.log(res.data)
setUserInfo(res.data) //userInfo에는 받아온 값 넣어주기
setIsLogin(true) //true로 바꿔주기
})
.catch((err) => {
// 로그인에 실패했다면 그에 대한 에러 핸들링을 구현하세요.
});
};
[Client_Mypage.js / 흐름5: 로그아웃버튼 구현]
import axios from 'axios';
import React from 'react';
export default function Mypage({ userInfo, setIsLogin, setUserInfo }) {
const logoutHandler = () => {
//흐름5: 로그아웃 버튼을 구현
//axios로 login을 담당하는 endpoint로 호스트요청 보내기
//post의 두번째 인자로 필요한 데이터를 전달할 수 있음
return axios
.post("http://localhost:4000/logout")
.then((res) => {
// 로그아웃에 성공했다면 App의 상태를 변경하세요.
//흐름7: 로그아웃 성공으로 쿠키를 삭제했다면 상태를 비워주기
setIsLogin(false)
setUserInfo(null)
})
.catch((err) => {
// 로그아웃에 실패했다면 그에 대한 에러 핸들링을 구현하세요.
});
};
return (
<div className='container'>
<div className='left-box'>
<span>
{`${userInfo.name}(${userInfo.userId})`}님,
<p>반갑습니다!</p>
</span>
</div>
<div className='right-box'>
<h1>AUTH STATES</h1>
<div className='input-field'>
<h3>내 정보</h3>
<div className='userinfo-field'>
<div>{`💻 ${userInfo.position}`}</div>
<div>{`📩 ${userInfo.email}`}</div>
<div>{`📍 ${userInfo.location}`}</div>
<article>
<h3>Bio</h3>
<span>{userInfo.bio}</span>
</article>
</div>
<button className='logout-btn' onClick={logoutHandler}>
LOGOUT
</button>
</div>
</div>
</div>
);
}
[Server_logout.js / 흐름6: 로그아웃처리하면서 쿠키 삭제하기]
module.exports = (req, res) => {
//흐름6: 로그아웃한다면 저장해둔 쿠키를 삭제하기
//res.clearCookie의 인자에는 어떤이름의 쿠키를 지울것인지, 만들때 사용했던 쿠키옵션을 정확하게 적어야함(하나라도 빠지면 안됨)
//expire, maxAge는 예외할 수 있어서 없어도 됨
const cookieOptions = {
domain: 'localhost',
path: '/',
secure: true,
httpOnly: true,
sameSite: 'strict'
}
res.clearCookie('cookieId', cookieOptions).send('complete!')
};
[Client_Mypage.js / 흐름7: 로그아웃되면 상태 비우기]
import axios from 'axios';
import React from 'react';
export default function Mypage({ userInfo, setIsLogin, setUserInfo }) {
const logoutHandler = () => {
//흐름5: 로그아웃 버튼을 구현
//axios로 login을 담당하는 endpoint로 호스트요청 보내기
//post의 두번째 인자로 필요한 데이터를 전달할 수 있음
return axios
.post("http://localhost:4000/logout")
.then((res) => {
// 로그아웃에 성공했다면 App의 상태를 변경하세요.
//흐름7: 로그아웃 성공으로 쿠키를 삭제했다면 상태를 비워주기
setIsLogin(false)
setUserInfo(null)
})
.catch((err) => {
// 로그아웃에 실패했다면 그에 대한 에러 핸들링을 구현하세요.
});
};
[Client_App.js / 흐름8: 조건부 렌더링으로 login상태 유지]
import './App.css';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Login from './pages/Login';
import Mypage from './pages/Mypage';
import React, { useEffect, useState } from 'react';
import axios from 'axios';
// 모든 요청에 withCredentials가 true로 설정됩니다.
axios.defaults.withCredentials = true;
function App() {
//흐름3: 받아온 userInfo에 맞게 상태를 변경해줘야하기에 porps로 내려주기
const [isLogin, setIsLogin] = useState(false);
const [userInfo, setUserInfo] = useState(null);
const authHandler = () => {
//흐름8: 초기 화면 렌더링시, 서버에 유저 정보를 요청하여 Login 또는 Mypage가 렌더링되도록 구현
//user정보를 담당하는 endpint는 userinfo,
//login에서 유저정보를 보내주지않고 userinfo에서 쿠키값을 사용해서 유저 정보를 돌려보내주게 redirect시켰기때문
return axios
.get("http://localhost:4000/userinfo")
.then((res) => {
//userinfo endpoint로 요청을 보내고 userinfo가 처리해줘서 then값으로 들어옴
//로그인이 될 수 있게 Login에서 해줬던 것을 그대로 가져오기
setUserInfo(res.data)
setIsLogin(true)
})
.catch((err) => {
// 인증에 실패했다면 그에 대한 에러 핸들링을 구현하세요.
});
};
useEffect(() => {
// 흐름8: 처음 랜더링될때 실행되도록 useEffect를 사용 따라서 컴포넌트 생성 시 아래 authHandler() 함수가 실행됩니다.
authHandler();
}, []);
return (
<BrowserRouter>
<div className='main'>
<Routes>
<Route
path='/'
element={
//로그인이 된다면 Mypage를 로그인이 안된다면 Login페이지를
isLogin ? (
//흐름4: 로그인으로 컴포넌트에서 변경할 수 있도록 props를 전달
<Mypage userInfo={userInfo}
//흐름7: 로그아웃으로 컴포넌트에서 변경할 수 있도록 props를 전달
setIsLogin={setIsLogin} setUserInfo={setUserInfo}/>
) : ( //흐름4: 로그인으로 컴포넌트에서 변경할 수 있도록 props를 전달
<Login setIsLogin={setIsLogin} setUserInfo={setUserInfo}/>
)
}
/>
</Routes>
</div>
</BrowserRouter>
);
}
export default App;