: 서버가 웹 브라우저에 정보를 저장하고 불러올 수 있는 수단
쿠키는 삭제하지 않으면 사라지지 않기 때문에, 장기간 보존해야 하는 정보를 저장하기에 적합하다.
서버는 클라이언트에 특정한 데이터를 저장하고, 이 데이터를 다시 불러와 사용할 수 있다.
하지만 데이터를 저장하고 아무때나 불러올 수 있는게 아니라, 데이터를 저장한 후 특정 조건들이 만족되어야 다시 불러올 수 있다.
이러한 특정 조건들은 쿠키 옵션으로 표현할 수 있다.
'Set-Cookie':[
'cookie=yummy',
'Secure=Secure; Secure',
'HttpOnly=HttpOnly; HttpOnly',
'Path=Path; Path=/cookie',
'Doamin=Domain; Domain=velog.io/@wlwl99'
]
Domain
: 클라이언트에서는 쿠키의 도메인 옵션과 서버의 도메인이 일치하는 경우 쿠키를 전송한다.
http://www.localhost.com:3000/users/login
이라면, 여기서 도메인은 localhost.com
이다.
- 도메인 : 웹 브라우저를 통해 특정 사이트에 진입을 할 때, IP 주소를 대신하여 사용하는 주소 (ex.
google.com
)- 서브 도메인 : 도메인 앞에 추가로 작성되는 부분 (ex.
www
)
Path
: 클라이언트에서는 쿠키의 세부 경로와 서버의 세부 경로가 일치하는 경우 쿠키를 전송한다.
http://www.localhost.com:3000/users/login
이라면, 여기서 Path는 /users/login
이다./
로 설정되어 있다./users
로 설정되어 있을 때, 요청하는 Path가 users/login
인 경우에도 쿠키를 전송할 수 있다.Path : 세부 경로, 서버가 라우팅할 때 사용하는 경로
MaxAge / Expires
보안을 위해 쿠키의 유효 기간을 설정할 수 있다.
MaxAge
: 쿠키가 유효한 시간을 초 단위로 설정할 수 있다. (~초 동안 유효)Expires
: 쿠키가 언제까지 유효한지 날짜를 지정할 수 있다. (~까지 유효)세션 쿠키(Session Cookie)
: 브라우저가 실행 중일 때 사용할 수 있는 임시 쿠키로
MaxAge
,Expires
옵션이 없다. 브라우저를 종료하면 해당 쿠키는 삭제된다.영속성 쿠키(Persistent Cookie)
: 브라우저 종료 여부와 상관없이
MaxAge
,Expires
에서 지정한 유효 시간만큼 사용 가능한 쿠키
Secure
: 사용하는 프로토콜에 따라 쿠키 전송 여부를 결정할 수 있다.
true
: HTTPS 프로토콜를 이용하는 경우에만 쿠키를 전송할 수 있다.false
: HTTP 프로토콜에서도 쿠키를 전송할 수 있다.HttpOnly
: 스크립트에서 브라우저의 쿠키 접근 여부를 결정할 수 있다.
true
: 클라이언트에서 DOM을 이용해 쿠키에 접근하는 것을 막아준다.false
: JavaScript에서 document.cookie
를 이용해 쿠키 접근이 가능하므로 XSS 공격에 취약하다.false
로 설정되어 있다.SameSite
: CORS 요청(Cross-Origin)을 받은 경우 요청에서 사용한 메소드와 해당 옵션의 조합을 기준으로 서버의 쿠키 전송 여부를 결정할 수 있다.
Lax
: Cross-Origin 요청인 경우, GET 메소드 요청에 대해서만 쿠키 전송 가능Strict
: Cross-Origin 요청인 경우, 쿠키 전송 불가None
: 모든 메소드 요청에 대해 쿠키 전송 가능SameSite='none'
옵션을 사용하려면 Secure 쿠키 옵션이 필요하다.)서버에서 이러한 옵션들을 지정한 다음,
Set-Cookie
라는 프로퍼티에 쿠키를 담아 전송한다.Cookie
라는 프로퍼티에 쿠키를 담아 전송한다.이렇게 쿠키를 이용하여 서버는 클라이언트에게 인증 정보를 담은 쿠키를 전송하고,
클라이언트는 서버에게 전달 받은 쿠키를 요청과 함께 전송하여, 무상태(Stateless)의 인터넷 연결을 Stateful하게 유지할 수 있다.
MaxAge
나 Expires
옵션을 설정하지 않은 쿠키(세션 쿠키)는 클라이언트(브라우저)가 종료되어 세션이 종료될 때까지 유지되는데, HttpOnly
옵션을 사용하지 않았다면 JavaScript를 이용해서 쿠키에 접근할 수 있기 때문에 쿠키에 민감한 정보를 담는 것은 위험하다.
// 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('로그인에 실패했습니다.');
}
});
};
// 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();
}, []);
// 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');
}
// 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);
}
};
// 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);
});
};
// 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');
};
감사합니다! 도움되었습니다!