주특기 2주차 Node.js 숙련 - JWT

2022. 12. 26. 02:01항해99/3~5주차 - 주특기

1. 쿠키와 세션

  • 쿠키와 세션이란?
    • 쿠키(Cookie): 브라우저가 서버로부터 응답으로 Set-Cookie 헤더를 받은 경우 해당 데이터를 저장한 뒤 모든 요청에 포함하여 보냄
      • 데이터를 여러 사이트에 공유할 수 있기 때문에 보안에 취약할 수 있음
      • 쿠키는 userId=user-1321;userName=sparta 와 같이 문자열 형식으로 존재하며 쿠키 간에는 세미콜론(;) 으로 구분됨
    • 세션(Session): 쿠키를 기반으로 구성된 기술임. 단, 클라이언트가 마음대로 데이터를 확인 할 수 있던 쿠키와는 다르게 세션은 데이터를 서버에만 저장하기 때문에 보안이 좋으나, 반대로 사용자가 많은 경우 서버에 저장해야 할 데이터가 많아져서 서버 컴퓨터가 감당하지 못하는 문제가 생기기 쉬움
  • 쿠키(Cookie) 만들어보기
    • 서버가 클라이언트의 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=/`,
  });
  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를 이용하여 쿠키 접근하기
    • 일반적으로 쿠키는 req.headers.cookie에 들어있다.
    • req.headers는 클라이언트가 요청한 Request헤더를 의미함
    • 클라이언트서버에 요청(Request)을 보낼 때 자신이 보유하고 있는 쿠키자동으로 서버에 전달
    • 여기서 클라이언트가 전달하는 쿠키 정보는 Request header에 포함되어 서버에 전달되게 됨
//get-cookie에 접근했을 때, 설정된 쿠키를 출력하는 API
//req.headers에서 전달된 쿠키를 출력하는 예제

app.get("/get-cookie", (req, res) => {
  const cookie = req.headers.cookie;
  console.log(cookie); // name=sparta
  return res.status(200).json({ cookie });
});
  • cookie-parser 미들웨어 적용하기
    • cookie-parser 미들웨어는 요청에 추가된 쿠키를 req.cookies 객체로 만들어 줍니다. 더이상 req.headers.cookie와 같이 번거롭게 사용하지 않아도 됨
    • 쿠키를 사용하기 위해서는req.headers.cookie와 같이 여러 프로퍼티를 넘어서야 사용할 수 있었음
    • cookie-parser 사용하기
//cookie-parser를 이용해 쿠키를 출력하는 API
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 });
});
  •  세션(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("서버가 켜졌어요!");
});

 

2. JWT

  • 간략한 정리
    • JSON 형태의 데이터를 안전하게 교환하여 사용할 수 있게 해줌
    • 인터넷 표준으로서 자리잡은 규격
    • 여러가지 암호화 알고리즘을 사용할 수 있음
    • header.payload.signature 의 형식으로 3가지의 데이터를 포함합니다. 
    • 때문에 JWT 형식으로 변환 된 데이터는 항상 2개의 . 이 포함된 데이터여야 합니다.
  • 규격
    • header(머리)는 signature(배)에서 어떤 암호화를 사용하여 생성된 데이터인지 표현
    • payload(가슴)는 개발자가 원하는 데이터를 저장
    • signature(배)는 이 토큰이 변조되지 않은 정상적인 토큰인지 확인할 수 있게 도와줌

  • 더 알아두어야 할 특성
    • JWT는 암호 키(Secret Key)를 모르더라도 복호화(Decode)가 가능함
    • 변조만 불가능 할 뿐, 누구나 복호화하여 보는것은 가능하다는 의미가 됨
    • 때문에 민감한 정보(개인정보, 비밀번호 등)는 담지 않도록 해야함
    • 특정 언어에서만 사용 가능한것은 아님
    • 단지 개념으로서 존재하고, 이 개념을 코드로 구현하여 공개된 코드를 우리가 사용하는게 일반적임
  • 쿠키, 세션 차이
    • 데이터를 교환하고 관리하는 방식인 쿠키/세션과 달리, JWT는 단순히 데이터를 표현하는 형식임 
    • JWT로 만든 데이터를 브라우저로 보내도 쿠키처럼 자동으로 저장되지는 않지만, 변조가 거의 불가능하고 서버에 데이터를 저장하지 않기 때문에 서버를 Stateless(무상태)로 관리할 수 있기 때문에 최근 많이 쓰이는 기술중 하나
    • Stateless(무상태)와 Stateful(상태 보존)의 차이를 간단히 설명하자면, Node.js 서버가 언제든 죽었다 살아나도 똑같은 동작을 하면 Stateless하다고 볼 수 있음
    • 반대로 서버가 죽었다 살아났을때 조금이라도 동작이 다른 경우 Stateful하다고 볼 수 있음
    • 서버가 스스로 어떤 기억을 갖고 다른 결정을 하냐 마냐의 차이라고 보면 더 쉬움 
    • 로그인 정보를 서버에 저장하게 되면 무조건 Stateful(상태 보존)이라고 볼 수 있음

3. JWT는 어떻게 사용법

  • 오픈소스 라이브러리를 이용합니다.
    • 프로젝트로 이용할 폴더를 생성하고, 모듈을 설치해줌
    • 나는 제일 사용량이 많은 jsonwebtoken 라이브러리를 사용할 것!
npm init
npm i jsonwebtoken -S
  • 원하는 JSON 데이터를 암호화
const jwt = require("jsonwebtoken");

const token = jwt.sign({ myPayloadData: 1234 }, "mysecretkey");
console.log(token);
  • 복호화하기
const jwt = require("jsonwebtoken");

const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJteVBheWxvYWREYXRhIjoxMjM0LCJpYXQiOjE2Njc1NjE0NDB9.nvYSsLsT8jp7IfkbB2seCNeuLqRBgrrzDjKRFXjvoUE";
const decodedValue = jwt.decode(token);
  • 변조된 데이터 검증하기
const jwt = require("jsonwebtoken");

const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJteVBheWxvYWREYXRhIjoxMjM0LCJpYXQiOjE2Njc1NjE0NDB9.nvYSsLsT8jp7IfkbB2seCNeuLqRBgrrzDjKRFXjvoUE";
const decodedValueByVerify = jwt.verify(token, "mysecretkey");

 

  • 암호화 된 데이터 사용방법
    • 보통 암호화 된 데이터는 클라이언트(브라우저)가 전달받아 다양한 수단(쿠키, 로컬스토리지 등)을 통해 저장하여 API 서버에 요청을 할 때 서버가 요구하는 HTTP 인증 양식에 맞게 보내주어 인증을 시도합니다!

4. Access Token, Refresh Token

  • Access Token이란
    • Access Token은 사용자의 권한이 확인(ex: 로그인) 되었을 경우 해당 사용자를 인증하는 용도로 발급하는 토큰
    • Cookie jwt를 발급하고 설정한 Expire 기간이 지날 때 인증이 만료되게 하는것 또한 Access Token이라고 부를 수있음
    • 사용자가 Access Token을 가지고 인증을 요청할 경우 Token을 생성할 때 사용한 비밀키(Secret Key)를 가지고 인증하기 때문에, 복잡한 설계없이 코드를 구현할 수 있고, 여러 분기를 거치지 않아도 된다는 장점이 있음
    • Stateless(무상태) 즉, Node.js 서버가 죽었다 살아나더라도 동일한 동작을하는 방식임
    • jwt를 이용해 사용자의 인증 여부는 확인할 수 있지만, 처음 발급한 사용자 본인인지 확인할 수는 없음
    • Access Token은 그 자체로도 사용자를 인증하는 모든 정보를 가지고 있다. 그렇기 때문에 토큰을 가지고 있는 시간이 늘어날 수록 탈취되었을 때는 피해가 더욱 커지게 됨
    • 만약 토큰이 탈취되었다고 인지하더라도 저희들은 해당 토큰이 탈취된 토큰인지 알 수 없고, 고의적으로 만료를 시킬 수도 없음. 그러므로 언제든지 사용자의 토큰이 탈취될 수 있다고 생각을 하고, 피해를 최소화 할 수 있는 방향으로 개발을 진행해야함
  • Refresh Token이란
    • Refresh Token은 Access Token 처럼 해당하는 사용자의 모든 인증 정보를 관리하는 것이 아닌, 특정한 사용자가 Access Token을 발급받을 수 있게 하기 위한 용도로만 사용되는 토큰
    • Refesh Token은 사용자의 인증정보를 사용자가 가지고 있는 것이 아닌, 서버에서 해당 사용자의 정보를 저장소 또는 별도의 DB에 저장하여 관리함
    • 서버에서 특정 Token 만료가 필요할 경우 저장된 Token을 제거하여 사용자의 인증 여부를 언제든지 제어가 가능하다는 장점이 있음
    • 그렇다면 어째서 바로 Access Token을 발급하지 않고, Refresh Token을 거쳐서 Access Token을 발급하는것일까
      • 사용자에게 발급한 Token이 탈취당할 경우 피해를 최소화 하기 위해서
    • 실제 세계에서 사용하는 OTP와 같이 짧은 시간 내에서만 인증 정보를 사용할 수 있게하고, 주기적으로 재발급하여, 토큰이 유출되더라도 오랜 기간동안 피해를 입는것이 아닌, 짧은 기간동안만 사용가능하도록 하여 피해를 최소화할 수 있게 됨
    • 언제든지 토큰이 탈취될 수 있다는 것을 가정하고, 탈취를 막는것이 어렵다면, 탈취된 토큰자체를 사용할 수 있는 기간을 줄여서 피해를 막을 것