...
노드 - cookie 다루기
생 노드에서 http 모듈을 쓸때는, 이렇게 일일히 쿠키 설정을 코딩해 줘야 했었다.
// 쿠키 문자열을 자바스크립트 객체로 변환하는 함수
const parseCookies = (cookie = '') =>
cookie
.split(';')
.map(v => v.split('='))
.reduce((acc, [k, v]) => {
acc[k.trim()] = decodeURIComponent(v);
return acc;
}, {});
http
.createServer(async (req, res) => {
// 먼저 저장되어있는 쿠키가 있는지 없는지 검사
const cookies = parseCookies(req.headers.cookie); // 변환 -> { mycookie: 'test' }
// form에서 action이 /Login으로, submit하면 발동
if (req.url.startsWith('/login')) {
const { query } = url.parse(req.url); // url을 객체로 만들어 query키만 빼옴
const { name } = qs.parse(query); // query키의 값인 문자열을 또 객체화해서 name키만 빼옴. 이 값은 쿠키에 저장될꺼임
const expires = new Date();
expires.setMinutes(expires.getMinutes() + 5);
res.writeHead(302, {
Location: '/',
'Set-Cookie': `name=${encodeURIComponent(name)}; Expires=${expires.toGMTString()}; HttpOnly; Path=/`,
});
res.end();
} else if (cookies.name) { // name이라는 쿠키가 있는 경우, 로그인 된 경우
Express - cookie-parser
쿠키 읽기/쓰기
cookie-parser는 요청과 함께 들어온 쿠키를 해석하여 곧바로 req.cookies객체로 만든다.
위에서는 따로 쿠키를 파싱해서 객체로 변환하는 함수를 만들어서 써야 했었는데, 이 부분이 생략된 것이다.
그리고 유효 기간이 지난 쿠키는 알아서 걸러낸다.
const express = require('express');
const path = require('path');
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const app = express();
app.set('port', process.env.PORT || 3000);
app.use(cookieParser()); // get요청이 오면 uri변수들이 파싱되어 req.cookies객체에 저장된다.
app.get('/', (req, res) => {
// 쿠키 읽기
if (req.cookies) {
console.log(req.cookies) // { mycookie: 'test'}
} else { // 클라이언트에 저장된 쿠키가 없다면
// 쿠키 쓰기
// 'Set-Cookie': `name=${encodeURIComponent(name)}; Expires=${expires.toGMTString()}; HttpOnly; Path=/`,
res.cookie('name', encodeURIComponent(name), {
expires: new Date(),
httpOnly: true,
path: '/',
})
}
res.send('Hello, index');
});
app.listen(app.get('port'), () => {
console.log(app.get('port'), '번 포트에서 대기 중');
});
코드 문법만 다르지 기본적으로 자바스크립트 쿠키 원리와 같다.
왜 express 쿠키 파서에서 저런식으로 쓰는지는 다음 포스팅을 참고하면 쿠키 원리에 대해 완벽하게 이해할수 있을 것이다.
쿠키 읽기
app.use(cookieParser());
console.log(req.cookies)
사용법은 cookieParser() 함수를 호출하여 사용한다
이번에에는 매개변수를 아무것도 넣지 않았지만, 서명된 쿠키를 사용하는 경우 매개변수에 비밀키를 넣어주면 검증을 한다
cookieParser를 통해 해석된 쿠키는 req.cookies를 통해 접근이 가능하다
쿠키 쓰기(만들기)
쿠키를 만드는 경우에는 res.cookie를 통해 만들며 res.cookie(키, 값, 옵션) 의 형태로 사용이 가능하다
쿠키 옵션
- maxAge : 만료 시간을 밀리초 단위로 설정
- expires : 만료 날짜를 GMT 시간으로 설정
- path : 쿠키의 경로 디폴트 값은 "/"
- domain : 도메인 네임 디폴트 값은 "loaded"
- secure : https에서만 쿠키를 사용할 수 있도록 한다
- httpOnly : 웹서버를 통해서만 쿠키에 접근할 수 있도록 한다
- signed : 쿠키에 대한 서명을 지정한다
res.cookie('name', 'beomseok', {
expires: new Date(Date.now() + 900000),
httpOnly: true,
secure: true
});
옵션 중 signed라는 옵션이 있는데, 이를 true로 설정하면 쿠키 뒤에 서명이 붙는다.
내 서버가 쿠키를 만들었다는 것을 검증할 수 있으므로 대부분의 경우 서명 옵션을 키는 것이 좋다.
서명을 위한 비밀 키는 cookieParser 미들웨어에서 인수로 넣은 process.env.COOKIE_SECRET이 된다.
쿠키 삭제
쿠키를 제거하기 위해서는res.clearCookie 메서드를 사용해야 한다.
쿠키를 지우려면 키, 값, 옵션도 정확히 일치해야 지워진다 (expires나 maxAge 제외).
res.clearCookie('name', 'beomseok', {
httpOnly: true,
secure: true
});
인증(서명)된 쿠키
cookieParser() 첫 번째 인수로 비밀 키를 넣어줄 수 있다.
서명된 쿠키가 있는 경우, 제공한 비밀 키를 통해 해당 쿠키가 내가 만든 쿠키임을 검증할 수 있다.
쿠키는 클라이언트에서 위조하기 쉬우므로, 비밀 키를 통해 만들어낸 서명을 쿠키 값 뒤에 붙이는 것이다.
true로 설정된 'singed'옵션을 포함하기만 하면 된다.
그 후에 res.cookie()는 cookieParser(secret)에 전달된 암호를 사용하여 값을 서명한다.
이후에 req.signedCookie 객체를 통해 값에 접근할 수 있다.
서명이 붙으면 쿠키가 name=name.sign 형태가 된다.
또한 서명된 쿠키는 req.cookies 대신 req.signedCookies 객체에 들어간다.
req.signedCookies은 요청에 의해 서명되지 않고 사용할 준비가 된 쿠키를 보낼 때 서명을 포함한다.
서명된 쿠키는 개발자의 의도를 보여주기 위해 다른 객체에 속해있다
쿠키에 서명하는 것은 숨기거나 암호화하는 것이 아니라 그저 간섭을 간단하게 방지하는 것이다.(서명에 사용하는 비밀 키는 비공개이기 때문이다.)
서명하지 않은 쿠키를 보내면 속성의 기본 값은 빈 객체로 설정된다.
// 쿠키파서에 비밀키를 넣어 요청온 쿠키값이 내가 서명한 쿠키인지 파악한다.
// 암호 키를 작성하는 것에는 크게 규격이 없으며 개발자의 자유이다. 단 쉽게 유추할 수 있는 값은 사용하지 말자.
app.use(cookieParser(process.env.COOKIE_SECRET));
app.get('/', (req, res) => {
req.signedCookies // 서명 된 쿠키 데이터
});
서명된 쿠키를 개인 저장소로 활용하기
클라이언트의 count정보를 쿠키를 저장소로 이용해 사용해보자
cookieParser()객체를 사용선언할 때 인자로 서명을 위한 암호 키를 전달하자.
인자에 암호 키를 작성했다면 앞으로 서명된 쿠키를 생성하고 활용할 때 서버와 클라이언트 PC만 알아볼 수 있도록 통신하게 된다.
그리고 req.signedCookies 객체로 들어와, count키를 검사하고 값을 업데이트 한다.
클라이언트에게서 전달받은 서명된 쿠키 정보에 접근하기 위해서는 req.signedCookies 속성을 통해 정보에 접근해야한다는 것을 잊지 말자.
만약 req.cookies로 접근한다면 undefined 혹은 빈 데이터가 값으로 반환되거나 오류가 발생하게 될 것이다.
count 값을 업데이트했으면 다시 클라이언트에 업데이트한 쿠키 정보를 보낸다.
res.cookie()를 통해 쿠키를 생성할 때 객체 옵션으로 singed를 true로 설정하면 서명된 쿠키를 생성하겠다는 의미이다.
이제 /count URL로 접속해보자.
값은 singedCookies 속성을 통해 쿠키 정보에 접근했고 값을 count 변수에 담았다.
그리고 그 값을 화면에 출력했을 때는 쿠키에 저장되는 순수한 값이 출력된다는 것을 알 수 있다.
개발자 도구를 열어 Network탭에서 서버와 클라이언트 PC간 통신한 쿠키 파일 정보를 열어보자.
Request Headers의 Cookie 정보와 Response Headers의 Set-Cookie 정보를 보면 ,
사람이 읽을 수 없는 데이터로 변조하여 서버 PC와 클라이언트 PC만이 통신할 수 있는 암호규칙으로 정보를 통신하고 있다는 것을 알 수 있다.
따라서 해커는 화면에 출력되는 값이 쿠키에 저장되는 값인지 쉽게 판단할 수 없고 어떤 정보를 의미하는지 유추할 수가 없게 된다.
비밀키를 서버 어딘가에 저장해놓고 웹브라우저로부터 쿠키값이 오면 이를 복호화해 쿠키정보를 얻는 방식으로 행한다. 자세한건 양방향 암호화 원리를 참고하자
body-parser
요청의 본문에 있는 데이터를 해석하여 req.body 객체로 만들어주는 미들웨어이다.
보통 폼 데이터 또는 AJAX 요청의 데이터를 처리한다.
그러나 멀티파트 (이미지, 동영상. 파일) 데이터는 처리하지 못한다. 그 경우 multer 모듈을 사용한다.
http모듈 방식을 썼을때에는, 하나하나 스트림으로 받아서 파싱해서 name값을 꺼내줬어야 했지만,
...
} else if (req.method === 'POST') {
if (req.url === '/user') {
let body = '';
// 요청의 body를 stream 형식으로 받음
req.on('data', data => {
body += data;
});
// 요청의 body를 다 받은 후 실행됨
return req.on('end', () => {
console.log('POST 본문(Body):', body);
const { name } = JSON.parse(body); // name은 클라이언트에서 폼 전송할때 보낸 객체다.
const id = Date.now();
users[id] = name;
res.writeHead(201, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('ok');
});
}
body.parser()혹은 express에서 내장된 바디파서를 사용하면, 간단하게 뽑아 낼수가 있다.
const express = require('express');
const path = require('path');
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const app = express();
app.set('port', process.env.PORT || 3000);
app.use(express.json()); // 클라이언트에서 application/json 데이터를 보냈을때 파싱해서 body 객체에 넣어줌
app.use(express.urlencoded({ extended: true })); // 클라이언트에서 application/x-www-form-urlencoded 데이터를 보냈을때 파싱해서 body 객체에 넣어줌
app.get('/', (req, res) => {
req.body.name // 위의 바디파서 미들웨어 덕분에 사용할 수 있다.
res.send('Hello, index');
});
html의 form 태그를 사용하여 post/get 방식으로 요청하거나, ajax 등의 요청을 할때 'application/x-www-form-urlencoded' 타입으로 데이터가 오게 된다.
이 데이터를 express.urlencoded()로 파싱해서 body 객체에 넣어 개발자가 간편하게 폼데이터를 사용하도록 하는 것이다.
역시 서버에 서버로 요청했을때 보통 JSON 타입으로 데이터를 넘겨주게 되는데, application/json 타입의 데이터로 오게된다.
이 데이터를 express.json()으로 파싱해 는body 객체에 넣어 개발자가 간편하게 폼데이터를 사용하도록 하는 것이다.
body-parser는 익스프레스 4.16.0 버전부터 일부 기능이 익스프레스에 내장되게 되었다.
예를 들어 위에서 사용한 express.json 과 express.urlencoded 형식 외에,
Raw, Text 형식의 데이터를 추가로 해석하고 싶을 때, body-parser를 설치하여 사용하면 된다.
Raw는 요청의 본문이 버퍼 데이터일 때,
Text는 텍스트 데이터일 때 해석하는 미들웨어이다.
URL-encoded는 주소 형식으로 데이터를 보내는 방식이다. 폼 전송은 URL-encoded 방식을 주로 사용한다.
urlencoded 메서드를 보면 { extended: false } 라는 옵션이 들어있는데,
이 옵션이 false면 노드의 querystring 모듈을 사용하여 쿼리스트링을 해석하고,
true면 qs 모듈 (npm 패키지 중 하나) 을 사용하여 쿼리스트링을 해석한다.
qs는 querystring의 기능을 확장한 모듈이다.
가능한 extended : true로 놔두자.
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.