1. 클라이언트 요청의 한계
- 클라이언트가 서버에 요청을 보낼 때, 누가 요청했는지 정확히 알기 어렵습니다.
- IP 주소나 브라우저 정보를 통해 일부 확인할 수 있지만, 같은 IP를 여러 컴퓨터가 공유하거나 한 컴퓨터를 여러 사람이 사용할 수도 있습니다.
2. 로그인으로 해결하기
- 로그인은 쿠키와 세션을 이용해 사용자를 식별합니다.
- 로그인 후 새로 고침해도 로그아웃되지 않는 이유는 서버가 사용자를 기억하고 있기 때문입니다.
- 서버는 응답 시 쿠키를 전송하고, 브라우저는 이를 저장해 요청 시마다 자동으로 쿠키를 포함해 보냅니다.
- 서버는 요청에 포함된 쿠키를 읽어 사용자를 식별합니다.
3. 쿠키의 특징
- 쿠키는 단순한 '키-값' 형태입니다.
- 브라우저는 쿠키를 자동으로 처리하며, 서버에서 쿠키를 전송하는 코드만 작성하면 됩니다.
- 쿠키는 요청 헤더에 담겨 전송되고, 브라우저는 응답 헤더에 따라 쿠키를 저장합니다.
쿠키 기반 간단한 로그인 구현(개선 전)
HTML 코드
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>쿠키 & 세션 이해하기</title>
</head>
<body>
<form action="/login">
<input type="text" id="name" name="name" placeholder="이름을 입력하세요" required>
<button type="submit">로그인</button>
</form>
</body>
</html>
Node.js 서버 코드
const http = require('http');
const fs = require('fs').promises;
const path = require('path');
const PORT = 8080;
const COOKIE_PATH = '/';
// 쿠키 파싱 함수
const parseCookies = (cookie = '') =>
cookie
.split(';')
.map((v) => v.split('='))
.reduce((acc, [k, v]) => {
acc[k.trim()] = decodeURIComponent(v);
return acc;
}, {});
// 서버 생성
const server = http.createServer(async (request, response) => {
const cookies = parseCookies(request.headers.cookie);
if (request.url.startsWith('/login')) {
handleLogin(request, response);
} else if (cookies.name) {
greetUser(response, cookies.name);
} else {
serveHtml(response);
}
});
// 로그인 요청 처리
const handleLogin = (request, response) => {
const url = new URL(request.url, `http://localhost:${PORT}`);
const name = url.searchParams.get('name');
if (!name) {
response.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
response.end('이름이 제공되지 않았습니다.');
return;
}
const expires = new Date();
expires.setMinutes(expires.getMinutes() + 5);
response.writeHead(302, {
Location: '/',
'Set-Cookie': `name=${encodeURIComponent(name)}; Expires=${expires.toGMTString()}; HttpOnly; Path=${COOKIE_PATH}`,
});
response.end();
};
// 사용자 환영 메시지
const greetUser = (response, name) => {
response.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
response.end(`${name}님 안녕하세요.`);
};
// HTML 파일 제공
const serveHtml = async (response) => {
try {
const filePath = path.join(__dirname, 'cookie.html');
const data = await fs.readFile(filePath);
response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
response.end(data);
} catch (err) {
response.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
response.end(`파일 읽기 실패: ${err.message}`);
}
};
// 서버 시작
server.listen(PORT, () => {
console.log(`${PORT}번 포트에서 서버가 실행 중입니다.`);
});
1. parseCookies 함수
parseCookies는 쿠키 문자열을 객체로 변환하는 함수입니다.
예를 들어, 쿠키 문자열이 아래와 같다면
"name=John; age=30"
다음과 같은 객체를 반환합니다.
{ name: "John", age: "30" }
2. 쿠키 옵션
쿠키는 서버와 클라이언트 간 상태를 관리하기 위해 사용되며, 몇 가지 중요한 옵션이 있습니다.
- HttpOnly: JavaScript가 쿠키에 접근하지 못하도록 설정.
- 이를 통해 XSS 공격을 방지할 수 있습니다.
- Expires: 쿠키의 만료 시간을 지정.
- 예를 들어, 만료 시간이 지나면 브라우저가 쿠키를 삭제합니다.
- 세션 쿠키로 사용하려면 Expires를 설정하지 않습니다.
- Path: 쿠키가 유효한 경로를 지정.
- 기본값은 쿠키를 설정한 경로이며, 특정 경로에서만 쿠키가 유효하도록 설정할 수 있습니다.
쿠키와 관련된 상세한 옵션이나 사용 방법은 아래에서 확인할 수 있습니다.
https://ko.javascript.info/cookie
쿠키와 document.cookie
ko.javascript.info
문제점
쿠키 기반의 로그인 방식은 몇 가지 보안 문제를 가질 수 있습니다.
브라우저의 Application 탭에서 쿠키가 쉽게 노출됩니다.
예를 들어, 사용자의 이름, 비밀번호 등 개인정보를 쿠키에 저장하는 것은 부적절합니다.
쿠키는 클라이언트 측에서 쉽게 접근할 수 있으므로, 민감한 데이터가 노출될 위험이 있습니다.
민감한 정보를 쿠키에 저장하지 않고, 서버가 관리하도록 설계해야 합니다.
- 쿠키에는 민감한 데이터를 저장하지 않고, 세션 ID만 저장합니다.
- 세션 ID를 통해 사용자를 식별하고, 실제 민감한 정보는 서버에서만 관리합니다.
- 브라우저는 세션 ID를 이용해 서버와 통신하며, 모든 민감한 데이터는 서버의 세션에 저장됩니다.
세션 기반 로그인 코드(개선 후)
민감한 정보를 쿠키에 저장하지 않고, 세션 ID를 쿠키에 저장하며 사용자 정보를 서버에서 관리합니다.
const http = require('http');
const fs = require('fs').promises;
const path = require('path');
const PORT = 8080;
const COOKIE_PATH = '/';
const session = {};
// 쿠키 파싱 함수
const parseCookies = (cookie = '') =>
cookie
.split(';')
.map((v) => v.split('='))
.reduce((acc, [k, v]) => {
acc[k.trim()] = decodeURIComponent(v);
return acc;
}, {});
// 서버 생성
const server = http.createServer(async (request, response) => {
const cookies = parseCookies(request.headers.cookie);
if (request.url.startsWith('/login')) {
handleLogin(request, response);
} else if (cookies.session && session[cookies.session]?.expires > new Date()) {
greetUser(response, cookies.session);
} else {
serveHtml(response);
}
});
// 로그인 요청 처리
const handleLogin = (request, response) => {
const url = new URL(request.url, `http://localhost:${PORT}`);
const name = url.searchParams.get('name');
if (!name) {
response.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
response.end('이름이 제공되지 않았습니다.');
return;
}
const sessionId = String(Date.now());
const expires = new Date();
expires.setMinutes(expires.getMinutes() + 5);
session[sessionId] = { name, expires };
response.writeHead(302, {
Location: '/',
'Set-Cookie': `session=${sessionId}; Expires=${expires.toGMTString()}; HttpOnly; Path=${COOKIE_PATH}`,
});
response.end();
};
// 사용자 환영 메시지
const greetUser = (response, sessionId) => {
const { name } = session[sessionId];
response.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8;' });
response.end(`${name}님 안녕하세요.`);
};
// HTML 파일 제공
const serveHtml = async (response) => {
try {
const filePath = path.join(__dirname, 'cookie.html');
const data = await fs.readFile(filePath);
response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
response.end(data);
} catch (err) {
response.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
response.end(`파일 읽기 실패: ${err.message}`);
}
};
// 서버 시작
server.listen(PORT, () => {
console.log(`${PORT}번 포트에서 서버가 실행 중입니다.`);
});
'Node.js > 정리' 카테고리의 다른 글
Node.js로 RESTful Server 만들어보기 (0) | 2025.01.13 |
---|---|
Node.js로 HTTP 서버 만들어보기 (0) | 2025.01.06 |
Node.js 란? (1) | 2025.01.05 |