Cookie / Session / Token(1)

이성은·2023년 1월 9일
0
post-thumbnail

1. Cookie

학습목표

  • 쿠키의 작동 원리를 이해할 수 있다
  • 회원가입 및 로그인 등의 유저 인증에 대해 설명할 수 있다.
  • 세션의 개념을 이해할 수 있다.
  • 쿠키와 세션은 서로 어떤 관계이며, 각각이 인증에 있어서 어떤 목적으로 존재하는지 이해할 수 있다.
  • 세션의 한계를 이해할 수 있다.
  • Cookie
    • 어떤 웹사이트에 들어갔을 때, 서버가 일방적으로 클라이언트에 전달하는 작은 데이터
    • 서버에서 클라이언트에 영속성있는 데이터를 저장하는 방법
    • 쿠키를 이용하는 것은 단순히 서버에서 클라이언트에 쿠키를 전송하는 것만 의미하지 않고 클라이언트에서 서버로 쿠키를 다시 전송하는 것도 포함
    • 서버가 웹 브라우저에 정보를 저장하고 불러올 수 있는 수단
    • 해당 도메인에 대한 쿠키가 존재하면, 웹 브라우저는 도메인에게 http 요청시 쿠키를 함께 전달
'Set-Cookie':[
            'cookie=yummy', 
            'Secure=Secure; Secure',
            'HttpOnly=HttpOnly; HttpOnly',
            'Path=Path; Path=/cookie',
            'Doamin=Domain; Domain=codestates.com'
        ]
  • Cookie 이용
    • 사용자 선호, 테마 등 장시간 보존해야하는 정보 저장에 적합
    • 서버에서 클라이언트에 쿠키를 전송하고, 클라이언트에서 서버로 쿠키를 다시 전송하는 것
    • 원래 보완 목적이 아니다
      => 쿠키는 HTTP 프로토콜의 무상태성을 보완해주는 도구
  • Cookie 전달 방법
    • 서버가 응답 헤더에 Set-Cookie 라는 프로퍼티에 이름, 경로 등의 옵션 저장
    • 클라이언트는 쿠키가 담긴 응답을 받고, 매 요청 시마다 쿠키의 이름과 값을 서버에 전달
    • 서버가 쿠키를 저장하면 이후로는 해당 웹사이트를 이용할 때 매번 요청에 자동으로 쿠키가 함께 전송
      => 쿠키내용을 바탕으로 서버는 클라이언트에 저장된 쿠키내용을 통해 로그인 상태 유지등을 할 수 있다.
    • 쿠키는 <script> 태그로 접근 가능
      => 5. XSS 공격에 취약하므로 쿠키에 민감한 정보나 개인 정보는 담지 않는 것이 좋다.
  • 쿠키 옵션
    • 쿠키를 통해 서버가 클라이언트에 특정한 데이터를 저장할 수 있다.
      • 하지만, 데이터를 저장한 이후 아무 때나 데이터를 가져올 수는 없다. 데이터를 저장한 이후 특정 조건(쿠키 옵션)들이 만족되어야 다시 가져올 수 있기 때문
    • Domain
      • 서버와 요청의 도메인이 일치하는 경우 쿠키 전송
      • 쿠키 옵션에서 도메인은 포트 및 서브 도메인 정보, 세부 경로를 포함하지 않는다.
    • Path
      • 서버와 요청의 세부경로가 일치하는 경우 쿠키 전송
      • 설정된 경로를 포함하는 하위 경로로 요청을 하더라도 쿠키를 서버에 전송할 수 있다.
    • MaxAge or Expires
      • 쿠키 유효기간 설정
      • 세션 쿠키: MaxAge 또는 Expires 옵션이 없는 쿠키로, 브라우저가 실행 중일 때 사용할 수 있는 임시 쿠키이다.브라우저를 종료하면 해당 쿠키는 삭제된다.
      • 영속성 쿠키: 브라우저의 종료 여부와 상관없이 MaxAge 또는 Expires에 지정된 유효시간만큼 사용가능한 쿠키
    • HttpOnly : 스크립트의 쿠키 접근 가능 여부 결정
    • Secure : HTTPS 프로토콜에서만 쿠키 전송 여부 결정
    • SameSite
      • CORS 요청의 경우 옵션 및 메서드에 따라 쿠키 전송 여부 결정 => CSRF 공격을 막는데 매우 효과적인 옵션
      • Lax : 사이트가 서로 달라도, GET 메서드 요청만 쿠키 전송 가능
      • Strict : 사이트가 서로 다르면 쿠키 전송 불가
      • None : 사이트가 달라도 모든 메서드 요청에 대해 쿠키 전송 가능, Secure 옵션 필요
        => 서버에서 쿠키 전송 안돼고, 네트워크 탭에서 Set Cookie 경고창뜨면 None 옵션으로 해결 가능!!

// client/App.js
import "./App.css";
import {BrowserRouter, Routes, Route} from "react-router-dom";
import Login from "./pages/Login";
import Mypage from "./pages/Mypage";
import {useEffect, useState} from "react";
import axios from "axios";

// 모든 요청에 withCredentials가 true로 설정됩니다.
axios.defaults.withCredentials = true;

function App() {
	const [isLogin, setIsLogin] = useState(false);
	const [userInfo, setUserInfo] = useState(null);

	const authHandler = () => {
		axios
			.get("https://localhost:4000/userinfo")
			.then((res) => {
				setIsLogin(true);
				setUserInfo(res.data);
			})
			.catch((err) => {
				if (err.response.status === 401) {
					console.log(err.response.data);
				}
			});
	};

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

	return (
		<BrowserRouter>
			<div className="main">
				<Routes>
					<Route
						path="/"
						element={
							isLogin ? (
								<Mypage setIsLogin={setIsLogin} setUserInfo={setUserInfo} userInfo={userInfo} />
							) : (
								<Login setIsLogin={setIsLogin} setUserInfo={setUserInfo} />
							)
						}
					/>
				</Routes>
			</div>
		</BrowserRouter>
	);
}

export default App;
// client/Login.js
import React, {useState} from "react";
import axios from "axios";

export default function Login({setUserInfo, setIsLogin}) {
	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});
	};
	const loginRequestHandler = () => {
		if (!loginInfo.userId || !loginInfo.password) {
			setErrorMessage("아이디와 비밀번호를 입력하세요");
			return;
		} else {
			setErrorMessage("");
		}
		return axios
			.post("https://localhost:4000/login", {loginInfo, checkedKeepLogin})
			.then((res) => {
				setIsLogin(true);
				setUserInfo(res.data);
			})
			.catch((err) => {
				if (err.response.status === 401) {
					setErrorMessage("로그인에 실패했습니다.");
				}
			});
	};

	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>
	);
}

// client/MyPage.js
import axios from "axios";
import React from "react";

export default function Mypage({userInfo, setIsLogin, setUserInfo}) {
	const logoutHandler = () => {
		return axios
			.post("https://localhost:4000/logout")
			.then((res) => {
				setUserInfo(null);
				setIsLogin(false);
			})
			.catch((err) => {
				alert(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/index.js
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());

const corsOptions = {
	origin: "http://localhost:3000",
	methods: ["GET", "POST", "OPTIONS"],
	credentials: true,
};
app.use(cors(corsOptions));

app.post("/login", controllers.login);
app.post("/logout", controllers.logout);
app.get("/userinfo", controllers.userInfo);

let server;
if (fs.existsSync("./key.pem") && fs.existsSync("./cert.pem")) {
	const privateKey = fs.readFileSync(__dirname + "/key.pem", "utf8");
	const certificate = fs.readFileSync(__dirname + "/cert.pem", "utf8");
	const credentials = {
		key: privateKey,
		cert: certificate,
	};

	server = https.createServer(credentials, app);
	server.listen(HTTPS_PORT, () => console.log(`🚀 HTTPS Server is starting on ${HTTPS_PORT}`));
} else {
	server = app.listen(HTTPS_PORT, () => console.log(`🚀 HTTP Server is starting on ${HTTPS_PORT}`));
}
module.exports = server;

// server/login.js
const {USER_DATA} = require("../../db/data");

module.exports = (req, res) => {
	const {userId, password} = req.body.loginInfo;
	const {checkedKeepLogin} = req.body;
	const userInfo = {
		...USER_DATA.filter((user) => user.userId === userId && user.password === password)[0],
	};
	const cookiesOption = {
		domain: "localhost",
		path: "/",
		httpOnly: true,
		sameSite: "none",
		secure: true,
	};
	if (!userInfo.id) {
		res.status(401).send("Not Authorized");
	} else if (checkedKeepLogin === true) {
		cookiesOption.maxAge = 1000 * 60 * 30; //단위는 ms(밀리세컨드 === 0.001초), 30분동안 쿠키를 유지
	//cookiesOption.expires = new Date(Date.now() + 1000 * 60 * 30); //지금 시간 + 30분 후에 쿠키 삭제
		res.cookie("cookieId", userInfo.id, cookiesOption);
		res.redirect("/userinfo");
	} else {
		res.cookie("cookieId", userInfo.id, cookiesOption);
		res.redirect("/userinfo");
	}
};

// server/logout.js
module.exports = (req, res) => {
	const cookiesOption = {
		domain: "localhost",
		path: "/",
		httpOnly: true,
		sameSite: "none",
		secure: true,
	};
	res.status(205).clearCookie("cookieId", cookiesOption).send("logout");
};
profile
함께 일하는 프론트엔드 개발자 이성은입니다🐥

0개의 댓글