2022. 5. 3. 15:21ㆍ책/Node.js 교과서
3. 쿠키와 세션
클라이언트에서 보내는 요청에는 기본적으로 '누가' 요청을 보냈는지까진 포함돼있지 않다. 물론 IP주소나 브라우저의 정보는 받아올 수 있지만 그게 구체적으로 어떤 사람인지는 알 방법이 없는 것이다.
이는 HTTP 프로토콜 환경이 "connectionless, stateless"한 특성을 가지기 때문.
비연결성 ( Connectionless )
비연결성은 클라이언트가 요청을 한 후 응답을 받으면 그 연결을 끊어 버리는 특징을 의미한다. HTTP 프로토콜은 인터넷 상에서 다수의 사용자들을 통신하기 위해서 비연결성이라는 방식을 채택.
무상태 ( Stateless )
무상태는 통신이 끝나면 상태를 유지하지 않는 특징을 의미. 연결을 끊는 순간 클라이언트와 서버의 통신을 끝내며 상태 정보는 유지하지 않는다.
어쨌든 이러한 특성 때문에 서버는 클라이언트가 누구인지 구분하기 위한 기술이 필요해지는데, 그 중 쿠키와 세션이 있는 것이다.
3.1 쿠키
클라이언트(브라우저) 로컬에 저장되는 키-값이 들어있는 작은 데이터 파일. 서버가 클라이언트에게 쿠키를 동봉해서 보내주면 클라이언트는 쿠키를 로컬에 저장해뒀다가 다음 Request시 쿠키를 동봉해서 보내게 된다. 따라서 서버는 동봉된 쿠키를 보고 사용자가 누구인지 알 수 있는 것.
브라우저는 쿠키를 받게되면 따로 처리를 안 해도 Request Header에 자동으로 동봉돼서 요청을 보내게 된다. 따라서 서버에서 브라우저로 쿠키를 보내는 부분만 신경 쓰면 된다.
- 쿠키 명세 : https://datatracker.ietf.org/doc/html/rfc6265
- 쿠키 옵션
더보기아래 설명한 것들 외에도 여러 속성이 있긴 하지만, 나름 중요한 것들만 따로 뺐다. 자세한 건https://ko.javascript.info/cookie 참고
○ path
지정한 경로와 그 하위 경로에 있는 페이지만 쿠키에 접근할 수 있다. 절대 경로이어야 하고, (미 지정 시) 기본값은 현재 경로. 특별한 경우가 아니라면, path 옵션을 path=/같이 루트로 설정해 웹사이트의 모든 페이지에서 쿠키에 접근할 수 있도록 한다.
○ domain
쿠키에 접근 가능한 도메인을 지정한다. 예를 들어 domain=coupang.com 이라고 치면 쿠팡에서 준 쿠키는 네이버에선 읽을 수 없다는 뜻
○ expires, max-age
쿠키의 유효기간을 설정하는 옵션. expires는 GMT 포맷의 일자를 받고, max-age는 유효시간을 second단위로 받아서 쓴다
expires = Tue, 19 Jan 2038 03:14:07 GMT (date.toUTCString 메서드 쓰면 편함)
max-age = 3600 (유효시간 1시간)
○ secure
HTTPS로 통신하는 경우에만 쿠키가 전송된다
○ httpOnly
클라이언트 측 스크립트가 쿠키를 사용할 수 없게 한다. 예를 들어, 자바스크립트라면 document.cookie로 쿠키를 취득하지 못하게 한다.
1) 쿠키 만들어 보내기(Set-Cookie)
바로 위에서 말했듯 서버에서 쿠키를 보내주기만 하면 된다. 응답 헤더를 작성할 때 'Set-Cookie' 프로퍼티에 담아주면 끝. 쿠키는 name=holymoly;year=2200처럼 문자열 형식으로 존재하며, 쿠키 간에는 세미콜론으로 구분된다.
const http = require('http');
const server = http.createServer((req, res) => {
console.log(req.url, req.headers.cookie); // 요청url과 header에 쿠키 확인
res.writeHead(200, { 'Set-Cookie': 'mycookie=test' });
res.end('Hello Cookie');
})
server.listen(8083, () => console.log('8083번 포트 서버 대기중'))
콘솔창
/ undefined
/favicon.ico mycookie=test
어려운 건 없지만 이상한 건 있다. req.url에 /favicon.ico가 찍혀있다. 브라우저는 파비콘이 뭔지 HTML에서 유추할 수 없으면 서버에 파비콘 정보에 대한 요청을 보내기 때문이다. 그리고 위 예제에서 서버가 파비콘에 대한 정보를 주지 않았으므로 브라우저가 알아서 /favicon.ico를 요청한 것.
어찌 됐든 헤더의 'Set-Cookie'를 통해 쿠키를 심었고, 브라우저가 알아서 이 쿠키를 동봉해 요청을 보냄을 확인했다. 그런데 '그래서 이 쿠키가 누구인데?'에 대한 답은 아직 나오지 않았으므로 이를 해결해보자.
2) 쿠키로 사용자 식별하기
아래와 같은 페이지를 만들건데, 핵심은 누군가가 로그인을 하면 그 정보를 토대로 쿠키를 보내주고, '아 누군가가 로그인한 흔적이 있는 쿠키네'라고 판단이 되면 그에 맞는 페이지를 보여주는 것이다.
코드가 생각보다 길어지지만 핵심은 아래와 같다.
- 쿠키는 단순히 문자열의 나열이므로 다루기가 어렵다 → 객체로 만들어서 해결
- 누군가가 로그인을 했다 → 로그인했다는 정보를 담은 쿠키를 전송
- 로그인한 흔적이 확인되면 그에 맞는 페이지를 보여준다 → if문으로 쿠키 내용 확인
const http = require('http');
const fs = require('fs/promises');
const { URL } = require('url');
// 쿠키는 기본적으로 'name=holy;second=molu;third=molla'같은 문자열
// 문자열을 분해해서 객체로 만들어주고 싶은거임
// { name: 'holy', second: 'molu', ... } 이런 식으로
const parseCookies = (cookie = '') =>
cookie
.split(';')
.map(value => value.split('='))
.reduce((acc, [k, v]) => {
console.log(k, v);
acc[k.trim()] = decodeURIComponent(v);
console.log(acc)
return acc;
}, {});
const server = http.createServer(async (req, res) => {
const cookies = parseCookies(req.headers.cookie);
if (req.url.startsWith('/login')) {
const params = new URL(`http://${req.headers.host}${req.url}`).searchParams;
const name = params.get('name');
// 쿠키의 유효기간을 설정하기 위한 코드
const expires = new Date();
expires.setMinutes(expires.getMinutes() + 5);
// 응답코드 302 Found
// 요청한 리소스의 URI가 일시적으로 변경되었음을 의미함
// 즉, 브라우저한테 페이지를 이동시키라는 명령.
res.writeHead(302, {
Location: '/', // 어디로 이동시킬지
// Set-Cookie 값으론 제한된 ASCII 코드만 들어갈 수 있다
// 즉, 한글같은거 넣으면 안됨
// 그래서 encodeURIComponent로 인코딩 해준 것
// 그리고 쿠키의 크기는 4KB를 넘으면 안됨
'Set-Cookie': `name=${encodeURIComponent(name)}; Expires=${expires.toUTCString}; HttpOnly; Path=/`
});
res.end();
// 쿠키에 누군가 로그인했다는 흔적이 있는지 확인
} else if (cookies.name) {
res.writeHead(200, { 'Contet-Type': 'text/plain; charset=utf-8' });
res.end(`Hi, '${cookies.name}' welcome.`);
} else {
try {
const data = await fs.readFile('./cookie2.html');
res.writeHead(200, { 'Content-type': 'text/html; charset=utf-8' });
res.end(data);
} catch (err) {
res.writeHead(500, { 'Content-type': 'text/plain; charset=utf-8' });
res.end(err.message);
}
}
})
server.listen(8084, () => console.log('8084포트'));
3.2 세션
일정 시간동안 같은 사용자(브라우저)로부터 들어오는 일련의 요구를 하나의 상태로 보고, 그 상태를 일정하게 유지시키는 기술. 여기서 일정 시간은 방문자가 웹 브라우저를 통해 웹 서버에 접속한 시점으로부터 웹 브라우저를 종료하여 연결을 끝내는 시점을 말한다.
말이 어려운데 사용자 정보를 서버에 저장하고 클라이언트와는 세선 아이디로만 소통하는 기술을 말한다. 쿠키를 기반으로 하고 있으며, 동작 방식은 아래와 같다.
- 클라이언트가 서버에 접속 시 세션 ID를 쿠키 안에 넣어서 발급 받는다
- 이후 클라이언트는 서버에 Request를 날릴 때 세션 ID가 담긴 쿠키를 서버에 전송한다
- 서버는 쿠키 안에 있는 세션ID를 확인하고, 본인 서버 안의 세션중 해당 ID와 맞는 세션이 있는지 확인한다
- 확인이 끝나면 적당한 정보를 던져주면 끝
위 과정을 그대로 코드로 옮기면 아래와 같다. 위 쿠키에서 쓴 코드를 그대로 재활용하되, 쿠키 안에 아이디를 그대로 담았던 방식과는 달리 세션ID만 담아서 전송하도록 변경했다. 물론 아래 코드조차도 세션ID가 그대로 공개되므로 보안상 매우 치명적인 문제를 가지고 있다.
const http = require('http');
const fs = require('fs/promises');
const { URL } = require('url');
const parseCookies = (cookie = '') =>
cookie
.split(';')
.map(value => value.split('='))
.reduce((acc, [k, v]) => {
console.log(k, v);
acc[k.trim()] = decodeURIComponent(v);
console.log(acc)
return acc;
}, {});
// 세션 관리를 위한 객체 생성
const session = {};
const server = http.createServer(async (req, res) => {
const cookies = parseCookies(req.headers.cookie);
if (req.url.startsWith('/login')) {
const params = new URL(`http://${req.headers.host}${req.url}`).searchParams;
const name = params.get('name');
const expires = new Date();
expires.setMinutes(expires.getMinutes() + 5);
const uniqueInt = Date.now();
session[uniqueInt] = { name, expires };
// [1] 쿠키에 세션 ID를 담아서 보낸다
res.writeHead(302, {
Location: '/',
'Set-Cookie': `session=${uniqueInt}; Expires=${expires.toUTCString}; HttpOnly; Path=/`
});
res.end();
// [2] 클라이언트가 보내온 쿠키(즉, 세션 쿠키)를 확인하고
// 세션ID에 맞는 세션을 뒤져본다. 추가로 여기선
// 아직 만료 기간이 지나지 않았는지도 확인한다
} else if (cookies.session && session[cookies.session].expires > new Date()) {
res.writeHead(200, { 'Contet-Type': 'text/plain; charset=utf-8' });
res.end(`Hi, '${cookies.name}' welcome.`);
} else {
try {
const data = await fs.readFile('./cookie2.html');
res.writeHead(200, { 'Content-type': 'text/html; charset=utf-8' });
res.end(data);
} catch (err) {
res.writeHead(500, { 'Content-type': 'text/plain; charset=utf-8' });
res.end(err.message);
}
}
})
server.listen(8085, () => console.log('8085포트'));
지금까지 쿠키와 세션에 대해 배웠지만 서비스를 새로 만들 때마다 이를 직접 구현할 순 없다. 게다가 보안상 매우 취약하기 때문에 검증된 코드를 사용하는게 좋다.
'책 > Node.js 교과서' 카테고리의 다른 글
[5장] npm 패키지 매니저 (0) | 2022.05.04 |
---|---|
[4장] 서버 만들기 (cluster) (0) | 2022.05.03 |
[4장] 서버 만들기 (REST & Routing) (0) | 2022.05.03 |
[4장] 서버 만들기 (http모듈) (0) | 2022.05.01 |
[3장] 노드 기능(2) (0) | 2022.04.29 |