본문 바로가기

내일배움캠프 노드 4기/Today I Learned

22/12/20 쿠키, 세션, JWT(JsonWebToken)

쿠키와 세션

쿠키(cookie) : 브라우저가 서버로부터 응답으로 Set-Cookie 헤더를 받은 경우 해당 데이터를 저장한 뒤 모든 요청에 포함하여 보냅니다.

  • 데이터를 여러 사이트에 공유할 수 있기 때문에 보안에 취약할 수 있습니다.
  • 쿠키는 userId=user-1321;userName=sparta 와 같이 문자열 형식으로 존재하며 쿠키 간에는 세미콜론(;) 으로 구분됩니다.

세션(session) : 쿠키를 기반으로 구성된 기술로 쿠키와는 다르게 데이터를 서버에 저장합니다.

  • 데이터를 서버에 저장하기 때문에 보안이 좋습니다.
  • 사용자가 많은 경우 서버에 저장할 데이터가 많아져 서버 컴퓨터가 감당하지 못하는 문제가 생길 수 있습니다.

쿠키 만들어보기

서버가 클라이언트의 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의 헤더를 의미합니다.

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 객체로 만들어줍니다.

// cookie-parser 미들웨어 전역 사용
app.use(cookieParser());

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 });
});

쿠키를 불러오는 부분이 req.cookies로 바뀌고 쿠키의 형식이 객체 형식으로 바뀌었습니다.

세션 만들어보기

API를 호출했을 때 name=sparta 의 정보를 서버에 저장하고, 저장한 시점의 시간 정보를 쿠키로 반환받는 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();
});

서버에 해당하는 유저의 정보를 저장하기 위한 session 객체를 생성했습니다.

API를 호출했을 때 name = 'sparta' 의 정보를 session에 삽입하고 해당하는 데이터를 불러들이기 위한 시간 정보를 쿠키로 받습니다.

 

API를 호출했을 때 쿠키의 시간 정보를 이용하여 서버에 저장된 name 정보를 출력하는 API

app.get('/get-session', function (req, res, next) {
  const { sessionKey } = req.cookies;
  const name = session[sessionKey];
  return res.status(200).json({ name });
});

쿠키에 저장된 sessionKey를 이용하여 session에 저장된 데이터를 불러옵니다.

JWT(JsonWebToken)

  • JSON 형태의 데이터를 안전하게 교환하여 사용할 수 있게 해줍니다.
  • 인터넷 표준으로서 자리잡은 규격입니다.
  • 여러가지 암호화 알고리즘을 사용할 수 있습니다.
  • header.payload.signature 의 형식으로 3가지의 데이터를 포함합니다. (개미처럼 머리, 가슴, 배) 때문에 JWT 형식으로 변환 된 데이터는 항상 2개의 . 이 포함된 데이터여야 합니다.

Encoded : "."을 기준으로 변환된다.

  • header(머리)는 signature(배)에서 어떤 암호화를 사용하여 생성된 데이터인지 표현합니다.
  • payload(가슴)는 개발자가 원하는 데이터를 저장합니다.
  • signature(배)는 이 토큰이 변조되지 않은 정상적인 토큰인지 확인할 수 있게 도와줍니다.

 

  • JWT는 암호 키가 없어도 복호화가 가능하기 때문에 민감한 정보(개인정보,비밀번호 등)는 담지 않아야 합니다.
  • 쿠키/세션과는 달리 단순히 데이터를 표현하는 방식입니다.
  • 서버를 Stateless(무상태)로 관리할 수 있기 때문에 최근 많이 쓰이는 기술입니다.
  • Stateless(무상태)란 서버가 언제든 꺼졌다 켜저도 똑같은 동작을 한다면 Stateless 한 것입니다.

JWT 사용하기

설치하기 : 프로젝트 터미널에 입력하기

npm init
npm i jsonwebtoken -S

데이터 암호화

sign을 이용한다.

const jwt = require("jsonwebtoken");

const token = jwt.sign({ myPayloadData: 1234 }, "mysecretkey");
console.log(token);

아래와 같은 결과를 얻는다.

데이터 복호화

decode를 이용한다.

const jwt = require("jsonwebtoken");

const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJteVBheWxvYWREYXRhIjoxMjM0LCJpYXQiOjE2Njc1NjE0NDB9.nvYSsLsT8jp7IfkbB2seCNeuLqRBgrrzDjKRFXjvoUE";
const decodedValue = jwt.decode(token);

데이터 검증

verify를 이용한다. decode와 같은 동작을 하지만 키가 맞지 않으면 복호화 할 수 없다.

const jwt = require("jsonwebtoken");

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

암호화 된 데이터 사용

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

ex) 놀이공원 자유 이용권

  • 회원가입: 회원권 구매
  • 로그인: 회원권으로 놀이공원 입장
  • 로그인 확인: 놀이기구 탑승 전마다 유효한 회원권인지 확인
  • 내 정보 조회: 내 회원권이 목에 잘 걸려 있는지 확인하고, 내 이름과 사진, 바코드 확인

Access Token, Refresh Token

  • Access Token은 사용자의 권한이 확인(ex: 로그인) 되었을 경우 해당 사용자를 인증하는 용도로 발급하게됩니다.
  • Refresh Token은 Access Token 처럼 해당하는 사용자의 모든 인증 정보를 관리하는 것이 아닌, 특정한 사용자가 Access Token을 발급받을 수 있게 하기 위한 용도로만 사용됩니다.

Access Token, Refresh Token 발급해보기

패키지 설치하기

npm init -y
npm install express jsonwebtoken cookie-parser -S

서버 파일 생성

// app.js

const jwt = require("jsonwebtoken");
const cookieParser = require("cookie-parser");
const express = require("express");
const app = express();
const port = 3002;
const SECRET_KEY = `여기에 키가 들어갑니다`;

// cookie-parsrt 미들웨어 사용하기
app.use(cookieParser());

app.get("/", (req, res) => {
  res.status(200).send("Hello Token!");
})

app.listen(port, () => {
  console.log(port, '포트로 서버가 열렸어요!');
})

cookie-parser 미들웨어는 요청에 추가된 쿠키를 req.cookies 객체로 만들어 줍니다.

 

토큰 발급받기

// app.js

...

let tokenObject = {}; // Refresh Token을 저장할 Object

app.get("/set-token/:id", (req, res) => {
  const id = req.params.id;
  const accessToken = createAccessToken(id);
  const refreshToken = createRefreshToken();

  tokenObject[refreshToken] = id; // Refresh Token을 가지고 해당 유저의 정보를 서버에 저장합니다.
  res.cookie('accessToken', accessToken); // Access Token을 Cookie에 전달한다.
  res.cookie('refreshToken', refreshToken); // Refresh Token을 Cookie에 전달한다.

  return res.status(200).send({ "message": "Token이 정상적으로 발급되었습니다." });
})

// Access Token을 생성합니다.
function createAccessToken(id) {
  const accessToken = jwt.sign(
    { id: id }, // JWT 데이터
    SECRET_KEY, // 비밀키
    { expiresIn: '10s' }) // Access Token이 10초 뒤에 만료되도록 설정합니다.

  return accessToken;
}

// Refresh Token을 생성합니다.
function createRefreshToken() {
  const refreshToken = jwt.sign(
    {}, // JWT 데이터
    SECRET_KEY, // 비밀키
    { expiresIn: '7d' }) // Refresh Token이 7일 뒤에 만료되도록 설정합니다.

  return refreshToken;
}

토큰 검증

// app. js

...

app.get("/get-token", (req, res) => {
  const accessToken = req.cookies.accessToken;
  const refreshToken = req.cookies.refreshToken;

  if (!refreshToken) return res.status(400).json({ "message": "Refresh Token이 존재하지 않습니다." });
  if (!accessToken) return res.status(400).json({ "message": "Access Token이 존재하지 않습니다." });

  const isAccessTokenValidate = validateAccessToken(accessToken);
  const isRefreshTokenValidate = validateRefreshToken(refreshToken);

  if (!isRefreshTokenValidate) return res.status(419).json({ "message": "Refresh Token이 만료되었습니다." });


  if (!isAccessTokenValidate) {
    const accessTokenId = tokenObject[refreshToken];
    if (!accessTokenId) return res.status(419).json({ "message": "Refresh Token의 정보가 서버에 존재하지 않습니다." });

    const newAccessToken = createAccessToken(accessTokenId);
    res.cookie('accessToken', newAccessToken);
    return res.json({ "message": "Access Token을 새롭게 발급하였습니다." });
  }

  const { id } = getAccessTokenPayload(accessToken);
	return res.json({ "message": `${id}의 Payload를 가진 Token이 성공적으로 인증되었습니다.` });
})


// Access Token을 검증합니다.
function validateAccessToken(accessToken) {
  try {
    jwt.verify(accessToken, SECRET_KEY); // JWT를 검증합니다.
    return true;
  } catch (error) {
    return false;
  }
}

// Refresh Token을 검증합니다.
function validateRefreshToken(refreshToken) {
  try {
    jwt.verify(refreshToken, SECRET_KEY); // JWT를 검증합니다.
    return true;
  } catch (error) {
    return false;
  }
}

// Access Token의 Payload를 가져옵니다.
function getAccessTokenPayload(accessToken) {
  try {
    const payload = jwt.verify(accessToken, SECRET_KEY); // JWT에서 Payload를 가져옵니다.
    return payload;
  } catch (error) {
    return null;
  }
}