본문 바로가기
경일/nodejs

[node.js] 쿠키와 세션, 그리고 JWT

by dev_kong 2022. 3. 3.
728x90
728x90

0. 목차

1. JWT가 뭔데.

2. JWT 구현

3. JWT로 로그인 해보기

1. JWT가 뭔데

JSON Web Token 의 약자다.

모바일이나 웹의 사용자 인증을 위해 사용하는 암호화된 토큰을 의미한다.

그런데 우리 이런 역할을 하는 다른애들을 이미 알고  있다.

 

바로 쿠키와

https://kong-dev.tistory.com/129

 

[nodejs] 쿠키 먹는거 말고

0.목차 1. http 통신 2. 브라우저 3. 쿠키 1. http 통신 서버에 요청을 보낼때는 일정한 규격에 따라 요청을 해야한다. 그런데 이 규격만 맞춰 준다면 새로운 내용을 작성해서 보내줄 수도 있다. 이제

kong-dev.tistory.com

 

세션이다.

https://kong-dev.tistory.com/131?category=998366 

 

[nodejs] 세션 이용해서 로그인기능 만들기

0. 목차 1. 개요 2. 세션 3. 세션으로 로그인기능 구현 1. 개요 오늘은 세션으로 로그인 기능을 구현하는걸 해보기로 함. 그럼 우선 세션이 뭔지 세션과 쿠키의 차이점은 뭔지 아는게 우선이다. 2.

kong-dev.tistory.com

 

쿠키도 만능이 아니고, 세션도 만능이 아니다.

각각 장단점이 있는데 

그 장단점들의 격차 속에서 어느정도 절충안을 찾은것이 바로 Token 방식 즉 JWT 이다.

 

쿠키의 가장 큰 단점은

바로 보안성이었다.

쿠키의 특징으로는 데이터가 브라우저에 저장되기에,

js 를 통해 손쉽게 조작할 수가 있다는 단점이 있었다.

 

반면 세션 같은 경우는

로그인된 사용자의 정보를 브라우저가 아닌 서버(메모리, 파일, DB)에 저장을 하기 때문에

조작이 어렵다.

그렇다고 만능은 아니다.

동시에 로그인 되어있는 사용자가 늘어나면 늘어날 수록 서버의 성능 또한 증축 되어야 한다.

 

즉, 서버의 성능에 굉장히 의존적이라고 할 수 있다.

 

이번에는 JWT 이다. 

JWT는 토큰 방식의 일종이다.

그럼 여기서 말하는 토큰은 뭘까

토큰은 그냥 길고 이상하게 생긴 텍스트다.

그리고 그 텍스트 안에는 총 3가지의 데이터가 담겨있다.

 

1.  HEADER

헤더에는 

토큰의 타입과, 

해시 암호화 알고리즘에 대한 정보가 담겨 있다.

 

  const header = {
      alg: 'sha256',
      typ: 'jwt',
    };

 

2. payload

페이로드에는

로그인된 사용자의 단편적인 정보가 담겨있다.

여기서 단편적이라고 얘기하는 이유는

이 페이로드의 부분은 쿠키로 전달될때 암호화 되어 전달 것이 아니라,

그저 16진수로 변환되어 있을 뿐이기에

정보의 유출과 조작이 간편하다.

그러므로 보안을 위해 

사용자의 예민한 정보들은 담아두지 않는 것이 좋다.

 

   const payload = {
      userid: matchedUser[0].userId,
      userAlias: matchedUser[0].userAlias,
    };

 

3. signature

그리고 마지막으로 시그니처

시그니처는 위의 두가지 (header와 payload) 데이터와

서버개발자가 지정한 salt를 잘 버무려서..(?)

hash 화 해버린 값이다.

 

저 salt의 역할은 위의 두 데이터가 조작이 되었는지 안되었는지 확인 할 수 있는 매우 중요한 수단으로 사용된다.

유출의 위험으로 부터 어는정도 자유로워 지기 위해,

환경변수로 선언한다.

 

그리고 jwt 의 형식은

인코딩한header.인코딩한payload.signature 의 형태를 지닌다.

 

 

2. JWT 구현

우선 header 와 payload 를 만들고,

const header = {
  alg: 'sha256',
  typ: 'jwt',
};

const payload = {
  userId: 'kong1234',
  userAlias: 'kong',
};

 

이걸 인코딩 64진수로 표현한 문자열로 변환한뒤,

빈값(영어로는 padding 이라고도 불리는 듯하다)을 표현하는 =을 제거해 준다.

 

const encodingHeader = Buffer.from(JSON.stringify(header))
  .toString('base64')
  .replace(/=/g, '');

const encodeingPayload = Buffer.from(JSON.stringify(payload))
  .toString('base64')
  .replace(/=/g, '');

 

그리고 인코딩된 header와 payload를 데리고 salt를 이용해서 hash화를 해준다.

hash화를 직접해줄 필요는 없고, crypto라는 모듈을 이용해서 할거다.

 

const signature = crypto
  .createHmac('sha256', Buffer.from('greenPea'))
  .update(`${encodingHeader},${encodeingPayload}`)
  .digest('base64')
  .replace(/=/g, '');

 

salt는 그냥 'greenPea' 로 했다.

sha256은 암호화에 이용한 알고리즘의 이름이다.

..ㅎ 뭔지는 잘 모른다.. ㅎ

그리고 마지막으로 

 

const jwt = `${encodingHeader}.${encodeingPayload}.${signature}`;

요렇게 만들어준뒤에 저 값을 쿠키로 집어 넣어주면 된다.

 

3. JWT로 로그인해보기

대충 어떻게 하는건지 알거 같으니까 실제로 로그인을 구현해보자.

3-1. 세팅

 

require('dotenv').config();

const express = require('express');
const crypto = require('crypto');
const nunjucks = require('nunjucks');
const userList = require('./user.js');

const app = express();

app.set('view engine', 'html');
nunjucks.configure('views', { express: app });

app.use(express.urlencoded({extended:true});

app.get('/', (req, res) => {
  res.render('index.html');
});

app.post('/', (req, res) => {});

app.listen(3000);

 

뭐 대충 세팅하고

3-2. 대충 HTML

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    {% if isLogin %}
    <!--  -->
    {{user.userid}}님 환영합니다
    <!--  -->
    {%else%}
    <form action="/" method="post">
      <input type="text" name="userid" />
      <input type="text" name="userpw" />
      <button type="submit">로그인</button>
    </form>
    {%endif%}
  </body>
</html>

 

HTML도 대충 만들어준뒤에

3-3. 로그인 기능구현

 

app.post('/', (req, res) => {
  const { userid, userpw } = req.body;

  const matchedUser = userList.filter((v) => {
    return v.userid === userid && v.userpw === userpw;
  });

  if (matchedUser.legnth > 0) {
    const header = {
      alg: 'sha256',
      typ: 'jwt',
    };

    const payload = {
      userid: matchedUser[0].userid,
    };

    const encodingHeader = Buffer.from(header)
      .toString('base64')
      .replace(/=/g, '');

    const encodingPayload = Buffer.from(payload)
      .toString('base64')
      .replace(/=/g, '');

    const signature = crypto
      .createHmac('sha256', Buffer.from(process.env.JWT_PW))
      .update(`${encodingHeader},${encodingPayload}`)
      .digest('base64')
      .replace(/=/g, '');

    const jwt = `${encodingHeader}.${encodingPayload}.${signature}`;

    res.setHeader('Set-Cookie', `jwt=${jwt}`);
    res.redirect('/');
  } else {
    res.send('로그인실패');
  }
});

이게 로그인을 시도 했을때다.

위에서 했던거를 똑같이 해준거다.

그리고 만들어진 jwt를 쿠키에 넣어준다.

 

app.get('/', (req, res) => {
  if (req.headers.cookie === undefined) {
    res.render('index.html');
  } else {
    const [, jwt] = req.headers.cookie.split('=');

    const [header, payload, signature] = jwt.split('.');

    const deSignature = crypto
      .createHmac('sha256', Buffer.from(process.env.JWT_PW))
      .update(`${header},${payload}`)
      .digest('base64')
      .replace(/=/g, '');

    if (deSignature === signature) {
      const isLogin = true;
      const user = JSON.parse(Buffer.from(payload, 'base64').toString());
      res.render('index.html', { isLogin, user });
    }
  }
});

 

그리고 이건 로그인이 됐을 때의 로직

 

signature 를 통해 payload가 조작되었는지 아닌지를 판별한뒤

조작된게 아니라는 게 확인되면,

payload를 디코딩해서 html로 전달 해주었다.

 

여기서 아이디랑 비밀번호를 맞게 입력하면,

 

이렇게 랜더링 됨. ㅎㅎ

코드가 아직 손에 안익다보니 

오타가 많이나서 에러가 아주아주 많이 터졌었다..

하다보면 익숙해지려나

 

아. 혹시나해서 남겨두는 링크하나.

2021.12.30 - [경일] - 16진수/ 아스키코드/ 유니코드

 

16진수/ 아스키코드/ 유니코드

0.목차 1. 개요 2. 2진법 3. 그래서 왜 16진법 4. 컴퓨터가 텍스트를 출력하는 방법 1. 개요 16진법 사실 일상생활에서 쓰일 일은 거의 없다. 아니 그냥 없다. 실제로 나도 살면서 한번도 안써봤으니까

kong-dev.tistory.com

이거 예전에 했었어서 자세히설명을 안적었는데 

내가 까먹을까봐 붙여놓음 ㅎ

728x90
728x90

댓글