Node.js 숙련 - 2
__________________________________________________
1-7 쿠키와 세션
=> < 2.2. JWT 01 >
쿠키란?
=> 브라우저가 서버로부터 응답으로 Set-Cookie 헤더를 받은 경우 해당 데이터를 저장한 뒤,
모든 요청에 포함하여 보냄.
데이터를 여러 사이트에 공유할 수 있기에 보안적으로 취약.
쿠키의 형태는 네임과 벨류를 가진, 객체 느낌. 문자열 형식으로 존재하며 쿠키 간에는 세미콜론으로 구분.
세션이란?
=> 쿠키를 기반으로 구성된 기술.
단, 세션은 데이터를 서버에만 저장하기 떼문에 보안상으로는 좋아.
그러나 사용자가 많은 경우 서버에 저장해야 할 데이터가 많아져서 서버 컴퓨터의 부하가 심해져.
폴더 : kimminsoo -> sparta -> node_js -> learning -> second_step -> session_prac
________________________
— 쿠키 만들어 보기 —
=> 서버가 클라이언트의 HTTP 요청(Request)을 수신할 때,
서버는 응답(Response)과 함께 Set-Cookie 라는 헤더를 함께 전송할 수 있어.
그 후 쿠키는 해당 서버에 의해 만들어진 응답(Response)과 함께 Cookie HTTP 헤더안에 포함되어 전달 받아.
< Set-Cookie 를 이용하여 쿠키 할당하기 >
——
app.get("/set-cookie", (req, res) => {
const expire = new Date();
expire.setMinutes(expire.getMinutes() + 60); // 만료 시간을 60분으로 설정합니다.
res.writeHead(200, {
'Set-Cookie': `name=sparta; Expires=${expire.toGMTString()}; HttpOnly; Path=/`,
}); // userId=user-1321;userName=sparta 와 같이 문자열 형식으로 존재하며 쿠키 간에는 세미콜론(;) 으로 구분
return res.status(200).end();
});
——
< res.cookie()를 이용하여 쿠키 할당하기 >
——
app.get("/set-cookie", (req, res) => {
const expires = new Date();
expires.setMinutes(expires.getMinutes() + 60); // 만료 시간을 60분으로 설정합니다.
res.cookie('name', 'sparta', {
expires: expires
});
return res.status(200).end();
});
——
— req를 이용하여 쿠키 접근하기 —
=> 클라이언트는 서버에 **요청(Request)**을 보낼 때 자신이 보유하고 있는 쿠키를 자동으로 서버에 전달한다.
여기서 클라이언트가 전달하는 쿠키 정보는 Request header에 포함되어 서버에 전달되게 될거야.
그렇다면 서버에서는 어떠한 방식으로 쿠키를 사용할 수 있을까?
일반적으로 쿠키는 req.headers.cookie에 들어있어. req.headers는 클라이언트가 요청한 Request의 헤더를 의미
——
app.get("/get-cookie", (req, res) => {
const cookie = req.headers.cookie;
console.log(cookie); // name=sparta
return res.status(200).json({ cookie });
});
——
— cookie-parser 미들웨어 적용하기 —
=> 쿠키를 사용하기 위해서는 req.headers.cookie 와 같이 여러 프로퍼티를 넘어서야 사용할 수 있었어.
게다가 쿠키의 네임이 여러 개일 경우 일일이 분리해야 하는 등 가독성도 그리 좋진 않았지.
cookie-parser 미들웨어는 요청에 추가된 쿠키를 req.cookies 객체로 만들어 줘.
더이상 req.headers.cookie와 같이 번거롭게 사용하지 않아도 돼.
보다 더 간결하게 만들 수 있어.
——
const cookieParser = require('cookie-parser');
app.use(cookieParser());
app.get("/get-cookie", (req, res) => {
const cookie = req.cookies;
console.log(cookie); // { name: 'sparta' }
return res.status(200).json({ cookie });
});
——
=> cookie를 불러오는 부분을 req.cookies로 변경되었고,
cookie의 형식이 name=sparta가 아니라 객체 형식으로 바뀌었어.. 좀 더 사용하기 쉬워졌다고 할 수 있지.
________________________
— 세션 만들어보기 —
=> 데이터를 사용자가 아닌 서버만 가지고 있는다.
서버에 있는 데이터가 유실될 때에는 현재 접속한 사용자의 인증정보까지 유실된다는 단점이 있긴 해.
쿠키의 경우 서버에서 오류가 발생하여 서버가 죽더라도, 클라이언트는 새로 고침을 하면 로그인이 유지 돼.
사용자의 입장에서는 편하지만, 쿠키가 조작되거나 노출되면 보안적으로 문제가 발생할 수 있어.
그렇다면, 보안성과 기밀성이 요구될 경우에는?
서버에서 해당하는 사용자가 누구인지 확실하게 구분할 수 있는 정보만 있다면,
서버에서 해당 사용자의 유니크한 정보도 반환할 수 있을거야.
크게 바로 세션. 세션은 쿠키의 원리를 베이스로 한 기능이지만, 사용자별로 고유의 session 값을 만드는데
이 세션값은 키를 사용하여 암호화 시킬 수 있어.
— /set-session API 만들기—
——
let session = {};
app.get('/set-session', function (req, res, next) {
const name = 'sparta';
const uniqueInt = Date.now();
session[uniqueInt] = { name };
res.cookie('sessionKey', uniqueInt);
return res.status(200).end();
});
——
— /get-session API 만들기 —
——
app.get('/get-session', function (req, res, next) {
const { sessionKey } = req.cookies;
const name = session[sessionKey];
return res.status(200).json({ name });
});
——
— api 전체 코드 —
——
const express = require('express');
const cookieParser = require('cookie-parser');
const app = express();
app.use(cookieParser());
let session = {};
app.get('/set-session', function (req, res, next) {
const name = 'sparta';
const uniqueInt = Date.now();
session[uniqueInt] = { name };
res.cookie('sessionKey', uniqueInt);
return res.status(200).end();
});
app.get('/get-session', function (req, res, next) {
const { sessionKey } = req.cookies;
const name = session[sessionKey];
return res.status(200).json({ name });
});
app.listen(5002, () => {
console.log("서버가 켜졌어요!");
});
——
- 연습문제
GET Method로 [<http://localhost:5001/set>](<http://localhost:5001/set>)을 호출했을 때, name에 nodejs가 저장된 쿠키를 할당하고
GET Method로[<http://localhost:5001/>](<http://localhost:5001/set>)get을 호출했을 때, 쿠키에 등록된 정보들이 반환되는 API를 만들어.
——
const express = require("express");
const cookieParser = require('cookie-parser');
const app = express();
app.use(cookieParser());
app.get("/set", (req, res) => {
res.cookie('name', 'nodejs');
return res.status(200).end();
});
app.get("/get", (req, res) => {
const cookie = req.cookies;
return res.status(200).json({ cookie });
});
app.listen(5001, () => {
console.log("서버가 켜졌어요!");
});
——
__________________________________________________
1-8 JWT가 무엇인가요?
=> < 2.2. JWT 02 >
간단한 정리!
=> JWT란, JSON 형태의 데이터를 안전하게 교환하여 사용할 수 있게 해주는 것.
인터넷 표준으로 자리잡은 규격.
여러가지 암호화 알고리즘을 사용할 수 있어.
*** 대표적으로 “ HS256 “ 이라는 알고리즘
JWT 는 “ header.payload.signature “ 의 형식으로 3가지의 데이터를 포함해.
따라서 JWT 형식으로 변환 된 데이터는 항상 2개의 “ . “ 이 포함된 데이터여야 해.
— 3가지 각 부분마다 어떤 내용을 저장하는지 알아보자 —
- header(머리)는 signature(배)에서 어떤 암호화를 사용하여 생성된 데이터인지 표현해.
=> 어떤 암호화를 사용해서 해당 JWT 토큰을 만들었는가에 대한 정보. 암호화 알고리즘이 여기에 서 명시될거야.
- payload(가슴)는 개발자가 원하는 데이터를 저장.
=> 예를 들어 session을 사용한다면, 필요한 키를 여기에 넣어 두기도.
또 해당 유저가 어떤 유저인지 확인하기 위해서 유저 id 에 대한 정보도 여기 저장할 수 있어.
실제로 가장 많이 사용하고, 데이터를 저장하는 영역.
- signature(배)는 이 토큰이 변조되지 않은 정상적인 토큰인지 확인할 수 있게 도와줘
=> JWT 토큰을 암호화 할 때, 특정한 키를 통해서 암호화를 했는지, 언제 만료되는지 등이 저장되는 곳.
시그니쳐 덕분에 해당 토큰이 변조되지 않은 정상적인 토큰인지 확인할 수 있어.
****
우리는 JWT 토큰을 쿠키에 넣고, 그 쿠키를 주고 받고 전달하면서 데이터를 주고 받는 거야.
JWT 토큰을 다루면서 가장 많이 접근하게 되는 영역은 payload 이지만,
실제로는 signature 부분에서 해당 토큰이 정상적인 토큰인지 검증하는 것 또한 많이 하게 될거야.
**** JWT 의 특징들 ****
- JWT는 암호 키(시크릿 키)를 모르더라도 복호화(decode) 를 할 수 있다.
=> payload 안에 어떤 데이터가 있는지 확인하는 데에는 키가 필요 없어. 아무런 사용자라도 모두 확인할 수 있어.
누구나가 JWT 토큰이 어떻게 적혀 있는지를 확인할 수 있어.
즉 반대로 말하면 키가 없더라도 누구에게나 노출이 될 수 있다는 단점이 있어.
그렇기에 암호화, 즉 검증을 해야 할 필요가 있어. 그걸 시그니처 부분이 담당하는거야.
- JWT 는 변조만 불가능 할 뿐, 누구나 복호화하여 보는 것은 가능해.
=> 때문에 보안성이 요구되는 정보는 담지 않도록 해야 해.
아이디나 비밀번호를 JWT 에 담으면 정보가 노출되어버려.
- JWT는 특정 언어에서만 사용 가능한 것은 아냐. JWT는 개념으로서 존재하고,
이 개념을 코드로 구현하여 공개된 코드를 우리가 사용하는게 일반적.
- 쿠키, 세션과는 어떻게 다른가?
=> JWT는 쿠키에 할당하면서 사용을 한다.
쿠키는 서버에서 전달받은 것을 클라이언트가 서버로 요청을 보낼 때마다 자동으로 호출해.
세션은, 데이터는 서버에만 있고 좌물쇠로 잠긴 상태이며, 클라이언트는 그 좌물쇠를 열 수 있는 열쇠만을 전달받아.
JWT는, 데이터를 교환하고 관리하는 쿠키나 세션과는 달리, 단순하게 데이터를 표현하는 방식.
우리는 특정한 JSON 형식의 데이터를 암호화를 해서 JWT 로 만드는 거야.
결국 JWT 라는 건 데이터를 표현하기 위해서 한 번 압축을 하는 과정인거지. 압축이 곧 암호화.
따라서 JWT 라는 건 특정 기능이 아냐. 그렇기에 브라우저로 보낸다고 해서 자동으로 저장되는 것도 아니고.
허나 변조가 거의 불가능하고 서버에 데이터를 저장하지 않기 때문에 서버를 Stateless(무상태) 로 관리할 수 있어.
보안상으로 크게 메리트가 있는 건 아니지만, 변조가 거의 불가능하고 서버에 가해지는 부담이 적다는 것이 특징.
- Stateless(무상태)와 Stateful(상태 보존)의 차이
=> Node.js 서버가 언제든 죽었다 살아나도 똑같은 동작을 하면 Stateless하다고 볼 수 있어.
반대로 서버가 죽었다 살아났을때 조금이라도 동작이 다른 경우 Stateful하다고 볼 수 있고.
JWT 가 클라이언트에게 전달이 됐다고 가정. 쿠키 안에 담겨 있는 상태.
전달이 되면, JWT 안에는 특정한 유저에 대한 정보와 특정한 열쇠에 대한 정보가 저장이 되어 있을 거야.
즉, 서버가 재시동 되더라도 클라이언트가 가지고 있는 JWT의 정보는 없어지지 않아.
다시 서버가 켜진 후에도 동일한 데이터를 전송해서 같은 효과를 얻을 수 있어.
즉, 서버가 어떤 것들을 데이터로서 기억하고 있느냐에 따라 판단이 달라진다.
session 처럼 로그인 정보를 서버에 저장하게 되면 무조건 Stateful 하다고 볼 수 있겠지.
__________________________________________________
1-9 JWT는 어떻게 사용하면 되나요?
=> < 2.2. JWT 03 >
오픈소스 라이브러리를 이용.
우리는 제일 사용량이 많은 jsonwebtoken 라이브러리를 사용해 볼거야.
폴더 : kimminsoo -> sparta -> node_js -> learning -> second_step -> jwt_project
- npm init
- npm i jsonwebtoken -S // 라이브러리 설치.
- app.js 생성.
- 원하는 JSON 데이터를 암호화 해보자.
——
< app.js >
const jwt = require("jsonwebtoken"); // jsonwebtoken 라이브러리 가져오기
const token = jwt.sign({ myPayloadData: 1234 }, "mysecretkey");
/*
실제로 JWT 를 만들어 볼거야. jwt.sign() 을 통해서 우리는 JWT 토큰을 만들 수 있어.
sign() 의 첫 번째 인자인 {} 는 payload.
실제로 우리가 JWT를 이용하면서 payload 내용을 설정하는 부분. 즉 우리가 원하는 데이터들을 저장하고 설정하는 부분이지.
두 번째 인자인 "mysecretkey" 는, JWT 키를 이용해서 암호화를 할 건데, 암호화를 하기 위한 비밀 키, 즉 시크릿 키.
*/
console.log(token);
// 출력
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJteVBheWxvYWREYXRhIjoxMjM0LCJpYXQiOjE2NzEwMjYwOTN9.QBfhmF5HqEwDpFI9IHQmNREmAxPqOuH1oFUUNhbjouA
——
=> 해당 토큰값은 굳이 시크릿 키가 없더라도 복호화가 가능해.
어떻게? jwt.io 라는 사이트에 가서 encode -> decode 를 할 수 있거든…
복호화 하면 나오는 디코드 정보 중 “ signature” 영역에 “ your-256-bit-secret “ 이라는 부분이 있거든?
여기다가 시크릿 키로 썼던 “mysecretkey” 를 덮어 씌우기로 입력하면,
해당 사이트의 아래부분에 “ invalid signature “ 에서 “signature verified “ 로 바뀌면서 키가 인증되었다고까지 나와.
- 다음으로, 토큰을 다시 복호화 해보자. 즉 유저가 토큰을 서버로 보냈을 때 이를 검증하기 위한 작업을 해보자는 거지.
——
const jwt = require("jsonwebtoken"); // jsonwebtoken 라이브러리 가져오기
const token = jwt.sign({ myPayloadData: 1234 }, "mysecretkey");
/*
실제로 JWT 를 만들어 볼거야. jwt.sign() 을 통해서 우리는 JWT 토큰을 만들 수 있어.
sign() 의 첫 번째 인자인 {} 는 payload.
실제로 우리가 JWT를 이용하면서 payload 내용을 설정하는 부분. 즉 우리가 원하는 데이터들을 저장하고 설정하는 부분이지.
두 번째 인자인 "mysecretkey" 는, JWT 키를 이용해서 암호화를 할 건데, 암호화를 하기 위한 비밀 키, 즉 시크릿 키.
*/
console.log(token);
console.log('----------------------------------------------------')
// 클라이언트가 토큰을 서버로 보냈다고 가정하고, 이를 복호화 해서 데이터를 열람 해보자.
const decodeToken = jwt.decode(token) // jwt 의 payload 에 담겨있는 데이터를 확인하기 위해서 디코드를 사용.
console.log(decodeToken); // { myPayloadData: 1234, iat: 1671026826 } 라고 출력.
// 우리가 payload 에 담았던 데이터와, 디코딩이 발생한 시간이 출력되는거야.
// 다음으로, 클라이언트에게서 받은 토큰이 우리의 시크릿 키로 생성된 토큰이 맞냐를 검증할거야.
const verifyToken = jwt.verify(token, "mysecretkey") // 만약 해당 키로 만들어진 토큰이 아니다? 에러가 발생.
console.log(verifyToken); // 해당 시크릿 키로 만든 토큰이 맞다면, decode 와 같은 내용이 출력.
——
- 추가로, 토큰을 sign() 으로 생성할 때 옵션으로서 만료 기한을 입력해 보자.
——
const jwt = require("jsonwebtoken"); // jsonwebtoken 라이브러리 가져오기
const token = jwt.sign({ myPayloadData: 1234 }, "mysecretkey", {
expiresIn: new Date().getMinutes() + 1 // expiresIn: 1s 이런 식으로 직접 시간을 입력해도 오케이.
});
/*
실제로 JWT 를 만들어 볼거야. jwt.sign() 을 통해서 우리는 JWT 토큰을 만들 수 있어.
sign() 의 첫 번째 인자인 {} 는 payload.
실제로 우리가 JWT를 이용하면서 payload 내용을 설정하는 부분. 즉 우리가 원하는 데이터들을 저장하고 설정하는 부분이지.
두 번째 인자인 "mysecretkey" 는, JWT 키를 이용해서 암호화를 할 건데, 암호화를 하기 위한 비밀 키, 즉 시크릿 키.
세 번째 인자인 {} 안에는 여러 옵션들이 들어가는데, 해당 예제의 expiresIn 는 토큰의 만료 기한을 정하는 옵션.
*/
console.log(token);
console.log('----------------------------------------------------')
// 클라이언트가 토큰을 서버로 보냈다고 가정하고, 이를 복호화 해서 데이터를 열람 해보자.
const decodeToken = jwt.decode(token) // jwt 의 payload 에 담겨있는 데이터를 확인하기 위해서 디코드를 사용.
console.log(decodeToken); // { myPayloadData: 1234, iat: 1671026826 } 라고 출력.
// 우리가 payload 에 담았던 데이터와, 디코딩이 발생한 시간이 출력되는거야.
// 만약 sing() 에 옵션으로 만료 기한을 추가했다면?
// { myPayloadData: 1234, iat: 1671027100, exp: 1671027112 } 이런 식으로 출력.
// 세 번째 프로퍼티인 exp 가 만료 기한인거지.
// 다음으로, 클라이언트에게서 받은 토큰이 우리의 시크릿 키로 생성된 토큰이 맞냐를 검증할거야.
const verifyToken = jwt.verify(token, "mysecretkey") // 만약 해당 키로 만들어진 토큰이 아니다? 에러가 발생.
console.log(verifyToken); // 해당 시크릿 키로 만든 토큰이 맞다면, decode 와 같은 내용이 출력.
——
=> sign() 에 세 번째 인자로서 {} 로 감싸진 옵션을 설정해 주고, 실제로 token 을 디코드 하면 exp 라는 옵션이 들어가 있음을 확인.
아마 만료 기한이 다 지나고 나서 verify로 검증을 하려고 한다면 기한이 만료 됐다면서 에러가 뜰거야.
즉 verify 는 우선 시크릿 키가 맞는지 검증한 후, 해당 jwt 토큰이 만료 되었는지도 검사한다는 뜻.
— 이 암호화 된 데이터는 어떻게 쓸 수 있나 —
=> 보통 암호화 된 데이터는 클라이언트가 전달받아 쿠키, 로컬 스토리지 등에 저장을 해.
보통 쿠키에 저장해 놨다가, 다음번에 서버에서 필요하다면 req 에 jwt 토큰을 담은 쿠키가 할당되도록 하는 방식을 많이 사용한다고.
비유하자면, 놀이공원의 자유이용권과 비슷한거야.
- 회원가입: 회원권 구매
- 로그인: 회원권으로 놀이공원 입장
- 로그인 확인: 놀이기구 탑승 전마다 유효한 회원권인지 확인
- 내 정보 조회: 내 회원권이 목에 잘 걸려 있는지 확인하고, 내 이름과 사진, 바코드 확인
결국 특정 사용자가 누구인지 확인하기 위해서 JWT 토큰을 사용하는 것이고, 추가로 특정 사용자에 대한 권한을
추가로 할당하기 위해서 사용할 수도.
사용자에 대한 특유한 정보를 관리하기 위해서 많이 사용해.
예를 들어서, 클라이언트가 특졍 게임방이나 채팅창 등의 채널에 참여하고 있었는데, 모종의 이유로 서버가 끊꼇다가 다시 연결되고 나서
다시 원래 있던 채널에 들어가야 되는 경우가 있어.
즉, 각 유저의 채널 참가 여부를 쿠키나 JWT 토큰에 넣어 두기도 해.
즉, JWT 토큰을 사용하는 방법들이 많다 라는 것.
__________________________________________________
1-10 Access Token, Refresh Token
=> < 2.2. JWT 04 >
Access Token 이란?
=> 사용자의 권한이 확인(ex: 로그인) 되었을 경우 해당 사용자를 인증하는 용도로 발급하게 되는 토큰.
특정 기능 이라기 보단 개념적인 기술.
Access Token의 경우 Stateless(무상태) 즉, Node.js 서버가 죽었다 살아나더라도 동일한 동작을하는 방식.
즉, jwt를 이용해 사용자의 인증 여부는 확인할 수 있지만, 처음 발급한 사용자 본인인지 확인할 수는 없어.
Access Token은 그 자체로도 사용자를 인증하는 모든 정보를 가지고 있어.
그렇기 때문에 토큰을 가지고 있는 시간이 늘어날 수록 탈취되었을 때는 피해가 더욱 커지게 될거야….
따라서 개발을 할 때에는 이 토큰이 언제든지 탈취되거나 결함이 생길 수 있다 가정을 하고,
피해를 최소화 시킬 수 있는 방향으로 개발을 해 나가야 해.
Refresh Token 이란?
=> Access Token 처럼 해당하는 사용자의 모든 인증 정보를 관리하는 것이 아닌,
특정한 사용자가 Access Token을 발급받을 수 있게 하기 위한 용도로만 사용된다.
즉, 사용자의 인증 정보를 사용자가 가지고 있는 것이 아니라, 특정 사용자가 access token 을 다시금 발급받을 수 있게
하기 위한 용도로서 사용되는 개념.
사용자의 인증정보를 사용자가 가지고 있는 것이 아닌, 서버에서 해당 사용자의 정보를 저장소 또는 별도의 DB에 저장하여 관리할거야.
때문에, 서버에서 특정 Token 만료가 필요할 경우 저장된 Token을 제거하여 사용자의 인증 여부를 언제든지 제어가 가능하다는 장점이 있어.
=> 따라서 이런 인증 정보를 확인하기 위해 noSql 과 같은 조회가 빠른, 형식이 상관없는 DB 등을 사용해.
아니면 radis 와 같은 캐시DB를 사용하기도.
저장소에 접근을 많이 하거나 조회를 많이 해야 하는 경우에는 nosql 이나 캐시 DB 가 더 성능이 좋은가봐.
만약 토큰이 탈취가 됐다 라고 서버가 인지한다면?
그럴 때는 탈취당한 토큰을 만료를 시켜줘야 하는데, 저장소나 DB에서 저장해 둔 토큰을 삭제하거나 하는 방식을 사용.
그렇다면 어째서 바로 Access Token을 발급하지 않고, Refresh Token을 거쳐서 Access Token을 발급하는 것인가?
용자에게 발급한 Token이 탈취당할 경우 피해를 최소화 하기 위해서.
OTP와 같이 짧은 시간 내에서만 인증 정보를 사용할 수 있게하고, 주기적으로 재발급하여,
토큰이 유출 되더라도 오랜 기간동안 피해를 입는것이 아닌, 짧은 기간동안만 사용가능하도록 하여 피해를 최소화할 수 있게 되는거야.
탈취를 막는것이 어렵다면, 우리는 탈취된 토큰자체를 사용할 수 있는 기간을 줄여서 피해를 막아야 해.
____________
— Refresh Token Project의 템플릿을 만들어 보자 —
——
< Refresh Token Project > API 목록
Refresh Token, Access Token 발급 GET http://localhost:3002/set-token/:id
Token 인증 받기 GET http://localhost:3002/get-token/
——
폴더 : kimminsoo -> sparta -> node_js -> learning -> second_step -> access_refresh_token
- app.js 파일 생성
- Npm init -y
- npm install express jsonwebtoken cookie-parser -S
자세한 사항은 app.js 주석을 참조.