일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 |
- 개인정보안전성
- 무료교육
- 한국정보보호산업협회기자단
- 한국정보보호산업협회
- 호이스팅
- 국가인적자원개발컨소시엄
- function scope
- 가명정보처리
- 백엔드입문
- package-lock.json
- 개인정보보호
- 마감임박
- 디오판투스 알고리즘
- 덧셈 암호
- pwnable.tw
- 곱셈 암호
- node.js
- 개인정보보호교육
- 한국산업인력공단
- 동적타이핑
- 모듈러 연산
- package.json
- 백엔드
- 확장 유클리드 알고리즘
- 포너블
- 유클리드_알고리즘
- Writeup
- 개인정보보호위원회
- 웹 프레임워크
- arrow function
- Today
- Total
짱짱해커가 되고 싶은 나
[http 모듈] 쿠키 본문
우리가 로그인을 하고 f5로 새로고침을 한다고 해서 로그아웃이 되지 않는다.
그 이유는 client가 server에게 지속적으로 우리가 누구인지 알려주기 때문이다.
이때 사용하는 것이 쿠키 또는 세션이다.
쿠키는 key-value의 쌍으로 이루어져 있다.
먼저 server는 미리 client의 요청에 요청자를 추정할만 정보를 쿠키로 만들어 보내고 client는 그 쿠키를 받아서 다음부터 그 쿠키와 함께 server에게 request를 한다. 그러면 server는 그 쿠키로 요청자를 확인할 수 있다.
(+브라우저는 쿠키가 있다면 자동으로 동봉해서 보내준다.)
쿠키는 req.headers.cookie에 저장되어 있다.
간단하게 쿠키를 만들어서 삽입하고, 쿠키를 확인하는 코드를 보자.
const http = require('http');
//문자열의 쿠키를 객체 형식으로 바꾸는 함수{:}
const parseCookies = (cookie ='') =>
cookie
.split(';')
.map(v => v.split('='))
.map(([k, ... vs])=> [k, vs.join('=')])
.reduce((acc, [k, v]) => {
acc[k.trim()] = decodeURIComponent(v);
return acc;
}, {});
http.createServer((req, res) =>{
const cookies = parseCookies(req.headers.cookie);
console.log(req.url, cookies);
res.writeHead(200, {'Set-Cookie': 'mycookie=test'});
res.end('Hello Cookie');
}).listen(8082, ()=>{
console.log('8082포트');
});
쿠키는 헤더에 저장되어 있다고 했다. 따라서 처음에 server가 client에게 response를 보낼 때 헤더에 {'Set-Cookie' : 'mycookie=test'} 와 같이 쿠키(키=값)을 설정해서 보내줘야 한다.
이처럼 헤더에 작성할 때는 writeHead(상태코드, {:}) 를 사용하면 된다.
추가적으로 write()는 client로 보낼 데이티를 작성해서 보낼 수 있고 end()는 인자가 있으면 데이터를 보내고 응답을 종료하는 함수다.
다음과 같이 response헤더에 cookie가 잘 설정된 것을 확인할 수 있다.
그리고 client에서 쿠키를 확인해보면 mycookie라는 쿠키에 test라는 값이 잘 들어간 것을 확인할 수 있다.
그러면 이렇게 받은 쿠키를 가지고 client는 server에게 req를 보내는데 이 때 쿠키는 그냥 문자열에 불가하다. 따라서 이 문자열을 객체 형식으로 바꿔줘야한다. 그 부분이 parseCookies 에 해당한다.
먼저 ;로 split을 한다.(cookie하나씩 자르기)
쿠키가 여러개면 headers.cookie에서는 다음과 같이 ;로 구분하기 때문이다.
Javascipt에서 split 함수는 분리한걸 배열의 형태로 저장한다.
그래서 map함수를 사용하는데 map은 각 순회에서 [key,value]로 이루어진 배열을 만드는데
쿠키가 들어오면 = 으로 키를 분리하고 [key, value]가 되는 것이다. join(=) 함수는 배열의 모든 요소를 =으로 연결해 하나의 문자열로 만듭니다. 즉, 키=값으로 합치는 것입니다.
reduce()는 배열의 각 요소에 reducer를 실행하고 하나의 결과값을 반환한다.
trim()은 문자열의 양 끝의 공백을 제거해준다. 즉 키의 양 끝 공백을 제거한다.
decodeURIComonent(v)를 하는데 이는 URI를 해독하는 것으로(문자열로 바꿔주는것) value를 해독해서 넣는 것이다.
그리고 개발자도구에서 네트워크를 확인해보면 우리가 호출한 localhost외에도 favicon.io를 부른다.
우리는 이런걸 요청한 적이 없는데 언제 요청된걸까?
파비콘은 인터넷 그 창 옆에 이모티콘?그림? 같은건데 http에 파비콘이 뭔지 유추할 수 없으면 자동으로 서버에 파비콘 정보를 요청한다. 따라서 브라우저에서 자동으로 favicon.io를 요청한 것이다.
그래서 결과를 보면 처음에 localhost를 요청할 때는 쿠키에 대한 정보가 없다가 다음에 favicon.io를 요청할 때 쿠키 값이 들어가 있는 것을 확인할 수 있다.
일단 지금은 이 키를 이용해서 판별하는 건 하지 않았기 때문에 login을 하는 것을 구현해보겠다.
<!DOCTYPE html>
<html>
<head>
<meta charset = "UTF-8" />
<title>쿠키&세션 이해하기</title>
</head>
<body>
<form action = "/login">
<input id="name" name="name" placeholder="이름을 입력하세요"/>
<buttoin id="login">로그인</buttoin>
</form>
</body>
</html>
간단한 로그인 창을 구현한 server4.html이다.
const http = require('http');
const fs = require('fs');
const url = require('url');
const qs = require('querystring');
const parseCookies = (cookie = '') =>
cookie
.split(';')
.map(v => v.split('='))
.map(([k, ... vs]) =>[k, vs.join('=')])
.reduce((acc, [k,v])=>{
acc[k.trim()] = decodeURIComponent(v);
return acc;
}, {});
http.createServer((req, res) =>{
const cookies = parseCookies(req.headers.cookie);
if(req.url.startsWith('./login')){
const {query} = url.parse(req.url);
const {name} = qs.parse(query);
const expires = new Date();
expires.setMinutes(expires.getMinutes() + 5);
res.writeHead(302, {
Location: '/',
'Set-Cookie': `name=${encodeURIComponent(name)}; Explires=${expires.toGMTString()}; HttpOnly; Path=/`,
});
res.end();
}else if(cookies.name){
res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
res.end(`${cookies.name}님 안녕하세요`);
}else{
fs.readFile('./server4.html', (err, data)=>{
if(err){
throw err;
}
res.end(data);
});
}
}).listen(8083, ()=>{
console.log('8083');
});
만약에 request의 url이 ./login으로 시작한다면 (즉, 로그인 페이지) query 객체에는 url.parse함수로 url문자열을 객체로 변환해준다. name객체는 이 쿼리 url 문자열을 쿼리 객체로 바꿔준다. 이게 뭔지 잘 모르겠어서 console로 결과를 뽑아봤다.
다음과 같이 url.parse를 하면 인코딩된 문자열이 그대로 뽑혀진다. 이 부분을 querystring의 parse를 하면 %~~부분(쿼리 문자열)을 디코딩해서 실제 문자열이 그대로 나온다.
그리고 로그인 유지 시간을 5분으로 할 것이기 때문에 현재 Date를 받아오고 그 중 분을 +5해서 저장한다. (만기시간)
그리고 writeHead를 하는데 302, 리다이렉션을 할 것이다.
이 때 리다이렉트 될 주소는 / 이고 cookie에는 name이 들어간다.
이 때 name을 인코딩해서 넣어준 이유는 헤더에는 한글이 들어갈 수 없기 때문이다.
그리고 cookie에 다양한 옵션을 설정해서 넣어주는데 Expires를 GMT문자열로 변환해서 넣어주고(날짜) HttpOnly(js에서 쿠키에 접근 불간)와 Path(쿠기가 전송될 URL 설정)를 설정해서 헤더를 보내준다.
Path가 /이면 모든 URL에 전송가능하다는 것을 의미한다.
/login이 아닌 경우에는 먼저 쿠키가 있는지 없는지를 확인한다.(cookies.name으로 확인) 쿠키가 없다면 로그인 페이지로 보내고(server4.html을 읽어서) 쿠키가 있다면 로그인한 상태로 간주해 인사말을 보낸다. 이 때 인사말이 한글이기 때문에 charset=utf-8로 설정해서 인코딩을 명시했다.
개발자 도구에서 application 탭을 보면 쿠키가 그대로 노출되어 있고 쿠키가 조작될 위험도 존재한다.
(클라이언트에서 쿠키를 관리하기 때문)
그래서 이 코드를 조금 더 수정해서 서버가 사용자 정보를 관리하도록 변경해보겠다. (세션)
const http = require('http');
const fs = require('fs');
const url = require('url');
const qs = require('querystring');
const parseCookies = (cookie ='') =>
cookie
.split(';')
.map(v => v.split('='))
.map(([k, ... vs])=> [k, vs.join('=')])
.reduce((acc, [k,v])=>{
acc[k.trim()] = decodeURIComponent(v);
return acc;
}, {});
const session = {};
http.createServer((req, res) =>{
const cookies = parseCookies(req.headers.cookie);
if(req.url.startsWith('/login')){
const {query} = url.parse(req.url);
const {name} = qs.parse(query);
const expires = new Date();
expires.setMinutes(expires.getMinutes() + 5);
const randomInt = Date.now();
session[randomInt] = {
name,
expires,
};
res.writeHead(302, {
Location: '/',
'Set-Cookie': `session=${randomInt}; Expires=${expires.toGMTString()}; HttpOnly; Path=/`,
});
res.end();
}else if(cookies.session && session[cookies.session].expires > new Date()){
res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
res.end(`${session[cookies.session].name}님 안녕하세요`);
}else{
fs.readFile('./server4.html', (err, data)=>{
if(err){
throw err;
}
res.end(data);
});
}
}).listen(8084, ()=>{
console.log(8084);
})
쿠키에 직접 로그인 이름을 넣는 대신 randomInt라는 임의의 숫자를 사용한다.
해당 session 번호를 이용해서 사용자 정보를 가져오는 것이다.
이러면 session번호만 알 수 있기 때문에 브라우저에서 실제 아이디에 접근할 수 없다.
하지만 여전히 session id에는 접근할 수 있기 때문에 취약한 코드이다. (모듈 사용 권장)
실제 서버에서는 이처럼 변수에 세션을 저장하지는 않는다. 그 이유는 서버가 멈추거나 재시작 된다면 메모리에 저장된 변수가 초기화되거나 메모리 부족으로 저장하지 못하는 문제도 있기 때문에 DB를 이용한다.
'Web > Node.js' 카테고리의 다른 글
사용자와 댓글 관리 서버 - 2 (0) | 2020.12.05 |
---|---|
사용자와 댓글 관리 서버 - 1 (0) | 2020.12.04 |
sequelize (0) | 2020.11.22 |
Node.js 훑어보기 (1) | 2020.08.07 |