쿠키 (Cookie)

지은·2022년 11월 10일
4

네트워크

목록 보기
9/17

🍪 쿠키 (Cookie)

: 서버가 웹 브라우저에 정보를 저장하고 불러올 수 있는 수단

  • 서버 → 클라이언트로 쿠키를 전송하고, 클라이언트 → 서버로 쿠키를 다시 전송하는 것을 포함한다.

쿠키에 저장하는 데이터

쿠키는 삭제하지 않으면 사라지지 않기 때문에, 장기간 보존해야 하는 정보를 저장하기에 적합하다.

  • 장바구니 정보
  • 로그인 상태 유지
  • 테마
  • 로그인/로그아웃을 위한 인증 정보
  • 마케팅을 위한 정보 등...

쿠키 옵션

서버는 클라이언트에 특정한 데이터를 저장하고, 이 데이터를 다시 불러와 사용할 수 있다.
하지만 데이터를 저장하고 아무때나 불러올 수 있는게 아니라, 데이터를 저장한 후 특정 조건들이 만족되어야 다시 불러올 수 있다.

이러한 특정 조건들은 쿠키 옵션으로 표현할 수 있다.

'Set-Cookie':[
			  'cookie=yummy', 
              'Secure=Secure; Secure',
              'HttpOnly=HttpOnly; HttpOnly',
              'Path=Path; Path=/cookie',
              'Doamin=Domain; Domain=velog.io/@wlwl99'
]

Set-Cookie MDN

1. Domain

: 클라이언트에서는 쿠키의 도메인 옵션서버의 도메인이 일치하는 경우 쿠키를 전송한다.

  • 이때, 쿠키 옵션에서 도메인은 포트 및 서브 도메인 정보, 세부 경로를 포함하지 않는다.
  • 만약 요청해야할 URL이 http://www.localhost.com:3000/users/login이라면, 여기서 도메인은 localhost.com이다.
  • 도메인 : 웹 브라우저를 통해 특정 사이트에 진입을 할 때, IP 주소를 대신하여 사용하는 주소 (ex. google.com)
  • 서브 도메인 : 도메인 앞에 추가로 작성되는 부분 (ex. www)

2. Path

: 클라이언트에서는 쿠키의 세부 경로서버의 세부 경로가 일치하는 경우 쿠키를 전송한다.

  • 만약 요청해야할 URL이 http://www.localhost.com:3000/users/login이라면, 여기서 Path는 /users/login이다.
  • Path를 명시하지 않으면 기본적으로 /로 설정되어 있다.
  • 설정된 경로를 포함하는 하위 경로로 요청을 하더라도 쿠키를 서버에 전송할 수 있다.
    ex. Path가 /users로 설정되어 있을 때, 요청하는 Path가 users/login인 경우에도 쿠키를 전송할 수 있다.

Path : 세부 경로, 서버가 라우팅할 때 사용하는 경로

3. MaxAge / Expires

보안을 위해 쿠키의 유효 기간을 설정할 수 있다.

  • MaxAge : 쿠키가 유효한 시간을 초 단위로 설정할 수 있다. (~초 동안 유효)
  • Expires : 쿠키가 언제까지 유효한지 날짜를 지정할 수 있다. (~까지 유효)
  • 이때 옵션의 값은 클라이언트의 시간을 기준으로 하며, 지정된 시간, 날짜를 초과하면 쿠키는 자동으로 소멸한다.
  • 이 옵션의 여부에 따라 쿠키는 세션 쿠키(Session Cookie)와 영속성 쿠키(Persistent Cookie)로 나눠진다.

: 브라우저가 실행 중일 때 사용할 수 있는 임시 쿠키로 MaxAge, Expires 옵션이 없다. 브라우저를 종료하면 해당 쿠키는 삭제된다.

: 브라우저 종료 여부와 상관없이 MaxAge, Expires에서 지정한 유효 시간만큼 사용 가능한 쿠키

4. Secure

: 사용하는 프로토콜에 따라 쿠키 전송 여부를 결정할 수 있다.

  • true : HTTPS 프로토콜를 이용하는 경우에만 쿠키를 전송할 수 있다.
  • false : HTTP 프로토콜에서도 쿠키를 전송할 수 있다.

5. HttpOnly

: 스크립트에서 브라우저의 쿠키 접근 여부를 결정할 수 있다.

  • true : 클라이언트에서 DOM을 이용해 쿠키에 접근하는 것을 막아준다.
  • false : JavaScript에서 document.cookie를 이용해 쿠키 접근이 가능하므로 XSS 공격에 취약하다.
    명시되지 않는 경우 기본적으로 false로 설정되어 있다.

6. SameSite

: CORS 요청(Cross-Origin)을 받은 경우 요청에서 사용한 메소드해당 옵션의 조합을 기준으로 서버의 쿠키 전송 여부를 결정할 수 있다.

옵션에 따른 서버의 쿠키 전송 여부

  • Lax : Cross-Origin 요청인 경우, GET 메소드 요청에 대해서만 쿠키 전송 가능
  • Strict : Cross-Origin 요청인 경우, 쿠키 전송 불가
  • None : 모든 메소드 요청에 대해 쿠키 전송 가능
    (SameSite='none' 옵션을 사용하려면 Secure 쿠키 옵션이 필요하다.)

쿠키 전달 방법

서버에서 이러한 옵션들을 지정한 다음,

  • 서버 → 클라이언트로 쿠키를 처음 전송하게 되면, 헤더에 Set-Cookie라는 프로퍼티에 쿠키를 담아 전송한다.
  • 이후 클라이언트 → 서버에 쿠키를 전송해야 하면, 헤더에 Cookie라는 프로퍼티에 쿠키를 담아 전송한다.


쿠키를 이용한 상태 유지

이렇게 쿠키를 이용하여 서버는 클라이언트에게 인증 정보를 담은 쿠키를 전송하고,
클라이언트는 서버에게 전달 받은 쿠키를 요청과 함께 전송하여, 무상태(Stateless)의 인터넷 연결을 Stateful하게 유지할 수 있다.


쿠키 이용 시 주의할 점

MaxAgeExpires 옵션을 설정하지 않은 쿠키(세션 쿠키)는 클라이언트(브라우저)가 종료되어 세션이 종료될 때까지 유지되는데, HttpOnly 옵션을 사용하지 않았다면 JavaScript를 이용해서 쿠키에 접근할 수 있기 때문에 쿠키에 민감한 정보를 담는 것은 위험하다.


쿠키로 로그인 상태 유지하기

클라이언트

Login.js

// client/src/pages/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) => {
          		console.log(res.data); // userinfo에서 응답해준 데이터
				setUserInfo(res.data);
				setIsLogin(true);
				setErrorMessage('');
			})
			.catch((err) => {
				if (err.response.status === 401) {
					setErrorMessage('로그인에 실패했습니다.');
				}
			});
      	};

App.js

// client/src/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);
				}
			});
	};
	
  	// 페이지가 렌더링될 때마다 최초 1번씩 authHandler를 실행시키는 useEffect
	// 새로고침 했을 때 클라이언트에 쿠키가 있다면, 서버로 요청을 보낼 때 쿠키도 자동으로 보낸다.
	useEffect(() => {
		authHandler();
	}, []);

서버

/login

// server/controllers/users/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],
	}; // DB에서 매칭되는 아이디와 비밀번호를 찾아 userInfo 배열에 넣는다.
	
	const cookieOptions = {
		domain: 'localhost',
		path: '/',
		sameSite: 'none', // sameSite : 'none' 옵션을 사용한다면, secure: true 옵션을 사용해야 한다.
		secure: true,
		expires: new Date(Date.now() + 24 * 3600 * 1000 * 7), // 7일 후 소멸되는 Persistent Cookie
		httpOnly: true,
	};
  
	if (userInfo.id === undefined) { // userInfo가 빈 배열일 경우, 401 Not Authorized로 응답한다.
		res.status(401).send('Not Authorized');
      
	} else if (checkedKeepLogin) {
		res.cookie('cookieId', userInfo.id, cookieOptions); // cookieId라는 쿠키에 userInfo.id 값을 담아 전송한다.
		res.redirect('/userInfo'); // userInfo 페이지로 리다이렉트해준다.
      
	} else {
      	delete cookieOptions.expires;
		res.cookie('cookieId', userInfo.id, cookieOptions);
		res.redirect('/userInfo');
	}

/userinfo

// server/controllers/users/userinfo.js

const { USER_DATA } = require('../../db/data');

module.exports = (req, res) => {
	// console.log(req.cookies);

	const cookieId = req.cookies.cookieId; // 요청에 있는 쿠키에서 cookieId 추출
	const userInfo = {
		...USER_DATA.filter((user) => user.id === cookieId)[0],
	}; // DB에서 같은 id를 가진 user를 찾는다.

	if (!cookieId || !userInfo.id) {
		// 쿠키가 없거나, userInfo가 비어있다면 401 Not Authorized로 응답한다.
		res.status(401).send('Not Authorized');
	} else {
		delete userInfo.password; // 응답에서 비밀번호는 제외하고 보낸다.
		res.send(userInfo);
	}
};

쿠키 삭제하여 로그아웃하기

클라이언트

Mypage.js

// client/pages.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) => {
				console.log(err.response.data);
			});

	};

서버

/logout

// server/controllers/users/logout.js

module.exports = (req, res) => {
	const cookieOptions = {
		domain: 'localhost',
		path: '/',
		sameSite: 'none',
		secure: true,
		expires: new Date(Date.now() + 24 * 3600 * 1000 * 7),
		httpOnly: true,
	};

	// 쿠키를 삭제할 때는 res.clearCookie 메소드를 사용한다.
	// clearCookie(쿠키 이름, 쿠키 옵션)
	res.status(205).clearCookie('cookieId', cookieOptions).send('logout');
};
profile
개발 공부 기록 블로그

1개의 댓글

comment-user-thumbnail
2023년 10월 26일

감사합니다! 도움되었습니다!

답글 달기