Back Ground

Node - 쿠키와 세션 [http모듈 웹 서버] 본문

Javascript/Node.js

Node - 쿠키와 세션 [http모듈 웹 서버]

Back 2019. 2. 23. 02:29

쿠키와 세션



쿠키는 요청과 응답의 헤더(header)에 저장된다.

요청과 응답은 각각 헤더와 본문(body)을 가진다.


ES6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const http = require('http');
 
const parseCookies = ( cookie = '' ) => {
    console.log("cookie : ",cookie);
    return 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번 포트에서 서버 대기 중입니다!')
    });
cs



- parseCookies라는 함수를 직접 만들어 보았다.

쿠키는 mycookie=test;year=1994처럼 문자열형식으로 오므로 

이를 { mycookis : 'test', year : '1994' }와 같이 객체로 바꾸는 함수이다.




- createServer메서드의 콜백에서는 제일 먼저 req객체에 담겨 있는 쿠키를 분석한다.

쿠키는 req.headers.cookie에 들어 있다.

req.headers는 요청의 헤더를 의미한다. 


응답의 헤더에 쿠키를 기록해야 하므로 res.writeHead메서드를 사용하였다.

첫 번째 인자로 200이라는 상태 코드를 넣었두었다. 

200은 성공이라는 의미한다.

다른 상태 코드에 대해서는 참고에서 설명한다.

두 번째 인자로는 헤더의 내용을 입력한다.

Set-Cookie는 브라우저한테 다음과 같은 값의 쿠키를 저장하라는 의미이다.


그래서 실제로 응답을 받은 브라우저는 mycookie=test라는 쿠키를 저장한다.


localhost:8082에 접속한다.

req.url과 cookies변수에 대한 정보를 로깅하도록 했다.

req.url은 주소의 path와 search 부분을 알려준다.






쿠키에 favicon.ico은 ?

요청은 분명 한 번만 보냈는데 두 개가 기록되어 있다.

/favicon.ico 파비콘이 나오는데 현재 예제에서 HTML에 파비콘에 대한 정보를 넣어두지 않았으므로

브라우저가 추가로 /favicon.ico를 요청한 것이다.


첫 번째 요청(/)을 보내기 전에는 브라우저가 어떠한 쿠키 정보도 가지고 있지 않다.

서버는 응답의 헤더에 mycookie=test라는 쿠키를 심으라고 브라우저에 명령했다.

따라서 브라우저는 쿠키를 심었고,

두번째 요청 (/favicon.ico)의 헤더에 쿠키가 들어 있음을 확인할 수 있다.







해더와 본문

- 요청과 응답의 형태 

출처 : https://www.quora.com/What-does-an-HTTP-request-looks-like


요청과 응답은 모두 헤더와 본문을 가지고 있다. 

- 헤더는 요청 또는 응답에 대한 정보를 가지고 있는 곳이고, 

- 본문은 서버와 클라이언트 간에 주고받을 실제 데이터를 담아두는 공간이다.


- 쿠키는 부가적인 정보이므로 헤더에 저장한다.



[확인 방법]

개발자 도구의 NetWork탭에서 요청과 응답을 살펴 볼 수 있다.

Network 탭을 띄워놓고 http:localhost8082에 접속하거나, 새로고침을 누르면 된다.


- General은 공통된 헤더이고, 

- Request Headers는 요청의 헤더, 

- Response Headers는 응답의 헤더이다.

 

수 많은 헤더가 있지만 Response Headers의 Set-Cookie와 Request Headers의 Cookie만 보면 된다.

응답의 Set-Cookie는 브라우저에게 해당 쿠키를 심으라는 명령을 내리는 것.


브라우저는 쿠키를 심은 후, 다음부터 요청을 보낼 때 Request Headers에 Cookie로 쿠키를 보낸다.




응답의 본문도 살펴볼 수 있다.

Response탭을 클릭해보면


res.end로 보내주었던 문자열이 보인다.

Cookies탭에서 쿠키만 따로 확인할 수 있다.






쿠키가 '나'인지 식별 하는 예제


server4.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>쿠키 & 세션 이해하기</title>
</head>
<body>
    <form action="/login">
        <input id="name" name="name" placeholder="이름을 입력하세요" />
        <button id="login">로그인</button>
    </form>   
</body>
</html>
cs


server4.js

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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);
    console.log("req :", req.url);
    console.log("cookies : ",cookies);
    if(req.url.startsWith('/login')){
        const { name } = qs.parse(req.url);
        const expires = new Date();
        expires.setMinutes(expires.getMinutes() + 5 );
        res.writeHead(302,{
            Location: '/',
            'Set-Cookie': `name=${encodeURIComponent(name)};Expires=${expires.toGMTString()}; HttpOnly; Path=/`,
        });
        
        const { query } = url.parse(req.url);
        console.log("query :", query);
        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번 포트에서 서버 대기 중입니다!');
});
cs



코드가 복잡한데

주소가 /login과 /로 시작하는 것까지 두 개이기 때문에 주소별로 분기 처리했다.

if(req.url.startsWith('/login')


req.url = /  (기본 url) 

}else if(cookies.name){



1)

 if(req.url.startsWith('/login')){
        const { name } = qs.parse(req.url);
        const expires = new Date();
        expires.setMinutes(expires.getMinutes() + 5 );
        res.writeHead(302,{
            Location: '/',
            'Set-Cookie': `name=${encodeURIComponent(name)};Expires=${expires.toGMTString()}; HttpOnly; Path=/`,
        });
        
        const { query } = url.parse(req.url);
        console.log("query :", query);
        res.end();

    }


주소가 /login으로 시작할 경우에는 url과 querystring 모듈로 각각 주소와 주소에 딸려오는 querty를 분석한다.

그리고 쿠키의 만료 시간도 지금으로부터 5분 뒤로 설정하였다.

이제 302응답 코드, 리다이렉트 주소와 함께 쿠키를 헤더에 넣는다.

브라우저는 이 응답 코드를 보고 페이지를 해당 주소로 리다이렉트한다.

헤더에는 한글을 설정할 수 없으므로 name변수를 encodeURIComponent 메서드로 인코딩했다.

 



2)

1
2
3
4
5
6
7
8
9
10
11
    }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);
        });
    }
cs


 그 외의 경우( /로 접속했을 때 등 ), 먼저 쿠키가 있는지 없는지를 확인한다.

쿠키가 없다면 로그인할 수 있는 페이지를 보낸다.

처음 방문한 경우엔 쿠키가 없으므로 server4.html이 전송된다.

쿠키가 있다면 로그인한 상태로 간주하여 인사말을 보낸다.

res.end 메서드에 한글이 들어가면 인코딩 문제가 발생하므로 

res.writeHead에 Content-Type을 text/html;charset=utf-8로 설정했다.




쿠키를 설정할 때 만료 시간(Expires)과 HttpOnly, Path 같은 옵션을 부여했다.

쿠키는 설정할 때 각종 옵션들을 넣을 수 있다. 

옵션 간에는 세미콜론(;)으로 구분하면 된다.


  • 쿠키명=쿠키값 : 기본적인 쿠키의 값입니다. mycookie=test 또는 name=background 같이 설정한다.
  • expires=날짜   : 만료 기한이다. 이 기한이 지나면 쿠키가 제거된다. 기본값은 클라이언트가 종료될 때까지이다.
  • Max-age=초    : Expires와 비슷하지만 날짜 대신 초를 입력할 수 있다. 해당 초가 지나면 쿠키가 제거된다. Expires보다 우선한다.
  • Domain=도메인명 : 쿠키가 전송될 도메인을 특정할 수 있다. 기본값은 현재 도메인이다. 
  • Path=URL : 쿠키가 전송될 URL을 특정 할 수 있다. 
  • Secure : HTTPS일 경우에만 쿠키가 전송된다.
  • HttpOnly : 설정 시 자바스크립트에서 쿠키에 접근할 수 없습니다. 쿠키 조작을 방지하기 위해 설정 하는 것이 좋다.



쿠키에 넣어도 되는가?

※ 원하는 대로 동작하기는 하지만 이 방식은 상당히 위험하다

현재 Application 탭에서 보이는 것처럼 쿠키가 노출되어 있다. 또한, 쿠키가 조작될 위험도 있다.

따라서 이름 같은 민감한 개인정보를  쿠키에 넣어두는 것은 적절하지 않다.







세션(session)


server5.js

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
const http = require('http');
const fs = require('fs');
const url = require('url');
const qs = require('querystring');
 
const parseCookies = (cookie = '' ) =>
     cookie
        .split(';')
        .map( v => { return v.split('=')} )
        .map( ([k, ...vs]) => [k, vs.join('=')] )
        .reduce( (acc, [k, v]) =>//축적 시킨다 (concat같은 개념) 
            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 = + new Date();
        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/server4.html', (err,data) => {
            if(err){
                throw err;
            }
            res.end(data);
        });
    }
})
.listen(8084, ()=> {
    console.log('8084번 포트에서 서버 대기 중입니다!');
})
cs


쿠키에 이름을 담아서 보내는 대신, 

randomInt라는 임의의 숫자를 보낸다.

사용자의 이름과 만료 시간은 session이라는 객체에 대신 저장한다.


이제 cookie.session이 있고 만료 기한을 넘기지 않았다면 session 변수에서 사용자 정보를 가져와서 사용한다.

다른 부분은 동일 한다.





서버에 요청을 보낼 때는 주소를 통해 요청의 내용을 표현한다.

주소가 /index.html이면 서버의 index.html을 보내달라는 뜻이고,

/about.html이면 about.html을 보내달라는 뜻이다.

요청이 항상 html을 요구할 필요는 없다.

server5.js에서도 /login이라는 주소를 통해서 

html을 요청하는 대신 세션 저장이라는 동작을 취하길 요청했다.

이렇게 요청이 주소를 통해 들어오므로 서버가 이해하기 쉬운 주소를 사용하는 것이 좋다.


그래서 여기서 REST API를 사용하게 된다.

REST API 설명 > https://backback.tistory.com/327





ES5 

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
var http = require('http'
 , express = require('express')
 , static = require('serve-static')
 , bodyParser = require('body-parser')
 , cookieParser = require('cookie-parser')
 , path = require('path');
 
var app = express();
 
//body-parser를 사용하겠다는 모듈
app.use(bodyParser.urlencoded({extended: false}));
 
var router = express.Router();
 
//쿠키를 사용하기 위해 parser한다.
app.use(cookieParser());
 
//메인
router.route('/').get(function(req,res){
    console.log("cookie");  
 
    res.writeHead('200',{"Content-Type":"text/html; charset=utf-8"}); 
    res.write('post 이다 : '+ '<br>');
   
    
    res.write('<a href="/ccc/set">쿠키생성</a> <br>');
    res.write('<a href="/ccc/show">쿠키보기</a> <br>');
    res.write('<a href="/ccc/modify">쿠키수정</a> <br>');
    res.write('<a href="/ccc/delete">쿠키삭제</a> <br>');
    
    res.end();
});
 
//쿠키생성
router.route('/ccc/set').get(function(req,res){ 
    res.cookie('user',{
        id: 'abcd',
        name'장동건',
    });
    res.writeHead('200',{"Content-Type":"text/html; charset=utf-8"}); 
    res.write('cooKie : <br>');
    res.write('<a href="/">메인</a>');
    
    //책방식
    //res.redirect('/ccc/show');
    
    res.end();
});
 
//쿠키보기
router.route('/ccc/show').get(function(req,res){ 
    
    res.writeHead('200',{"Content-Type":"text/html; charset=utf-8"}); 
    res.write('cookie 보기 <br>');
    
    if(req.cookies.user){
        res.write('id   : '+req.cookies.user.id+'<br>');
        res.write('name : '+req.cookies.user.name+'<br>');
    }else res.write('쿠키가 존재하지 않습니다.');
    
  
    res.write('<a href="/">메인</a>');
    
    res.end();
});
 
//쿠키수정 (생성과 같음 덮어쓰기)
router.route('/ccc/modify').get(function(req,res){ 
    res.cookie('user',{
        id: 'backback',
        name'한가인',
    });
    res.writeHead('200',{"Content-Type":"text/html; charset=utf-8"}); 
    res.write('cooKie : <br>');
    res.write('<a href="/">메인</a>');
 
    res.end();
});
 
 
 
//쿠키삭제
router.route('/ccc/delete').get(function(req,res){ 
    
    res.clearCookie('user');
    res.writeHead('200',{"Content-Type":"text/html; charset=utf-8"}); 
    res.write('cooKie 삭제 : <br>');
    res.write('<a href="/">메인</a>');
    
    res.end();
});
 
 
app.use('/',router); //index나 에러 메세지 보낼때 기본 패스 처리 
 
//get post 둘다 이곳에서 처리하게 한다.
app.all('*',function(req,res){
    res.status(400).send('페이지를 찾을수 없습니다.');
});
//put/delete는 쓰지않는다. (해킹할때 공격대상이 될수있다.)
 
 
http.createServer(app).listen(3000,function(){
   console.log('서버 기동'); 
});
cs






참고 : node.js 교과서 , doit Node.JS


Comments