본문 바로가기

Node.js

Node.js(20) - 로그인 기능(cookie, session), 암호화, JWT 개념 및 규격

728x90
반응형

1) Node.js

   1-1) JWT

      1-1-1) 로그인 기능 - cookie, session

      1-1-2) cookie, session의 차이점

      1-1-3) JWT 개념

      1-1-4) 암호화

      1-1-5) JWT 규격 정리

   1-2) JWT 개념 이해 관련 코드

 

 

 

 

 

 

 

1) Node.js

1-1) JWT

1-1-1) 로그인 기능 - cookie, session
"JWT"는 로그인 기능을 구현하기 위해 사용한다!


로그인에 대해 "cookie"와 "session"의 개념을 배웠다!
"HTTP의 비연결성"을 해결하기 위해 등장한 것이 바로 "cookie"와 "session"이다!

HTTP의 특징 : "비연결성"을 가진다!

HTTP는 한 번 request와 response를 주고 받으면 연결을 끊기 때문에

다시 request와 response를 주고 받으려면 반드시 다시 연결(3-way Handshaking)을 먼저 한 뒤
request와 response를 주고 받아야 한다!!(HTTP의 비연결성)

이때 request와 response의 경우, 파일 단위로 계속 주고 받는다는 점을 명심할 것!!

이러한 "비연결성"을 보완하기 위해 "cookie"가 등장함!!

"cookie"는 브라우저(Client) 안에 작은 저장소에 데이터를 저장하고(마치 이전에 배운 로컬 스토리지와 유사함),
request message에 해당 데이터를 넣어서 보낸다!
(즉, 브라우저가 요청을 보낼때마다 매번 request message에 cookie를 넣어서 보내줌!!
--> 정확히는 request header 영역에 넣어서 보내줌)


이것이 바로 "cookie"이다!
--> cookie는 request message의 header 부분에 string 타입으로 cookie가 들어감!

로그인 기능에 cookie가 사용됨!

"cookie"는 데이터를 client(브라우저)에 저장시키는 것이고,
"session"은 서버에 데이터를 저장시킨다는 차이가 있다!

 


OSI 7계층에서 4계층이 TCP이고, 7계층이 HTTP이다!
TCP는 통신을 하기 위한 일종의 규약인데 가장 큰 특징은 "3-way Handshaking"이다!
기본적으로 TCP는 네트워크를 자유롭게 쓴다!
반면, HTTP는 요청이 한 번 들어오면 반드시 응답을 한 번 준다는 차이가 있다!

(즉, 요청, 응답을 한 번 주고 받으면 HTTP는 반드시 그 후 연결을 끊는다!)

이처럼 HTTP는 기본적으로 TCP 위에서 돌아간다!

+) request header 안에 "Connection" 속성이 있는데 해당 속성의 속성값으로 "keep-alive"와 "close"가 있다!

 


로그인의 경우, 페이지를 이동하더라도 특정 변수의 값을 유지(특정 값들을 넘길 수 있다는 의미)할 필요가 있다!
원래는 페이지를 이동하면 기존 페이지의 값들은 날아간다!

HTTP는 기본적으로 "비연결성"이라는 특징을 가지지만
HTTP가 연결성을 가진 것처럼 보이게 하는 것이 "cookie"이다!

client(브라우저)가 요청을 할 때 cookie를 함께 던져주므로 server에서는 사용자가 누구인지 알 수 있다!
cookie는 브라우저가 만들어준 저장소이다!

로그인 시스템은 아이디와 패스워드를 정확히 입력할 시 cookie를 발급해주는 개념이다!
cookie의 내용이 길면 길수록 request message의 내용이 커진다!(즉, request message가 커지기에 요청에 부하가 생긴다!)

페이지가 이동이 되더라도 혹은 다른 사이트를 방문할 경우, 어떠한 데이터를 유지하기 위해서

브라우저의 storage를 활용한 것이 "cookie"이다!

"session"은 데이터베이스(DB)의 식별자를 back-end에 저장한 것으로,

이를 암호화(약 32 byte 크기)하여 브라우저에 넘겨 cookie에 담아놓는 개념이다!
"cookie"와 "session" 모두 cookie를 사용하지만 데이터의 중요한 정보의 주체를 브라우저에 맡기면 "cookie"이고, 이를 서버에 맡기면 "session"이다!

 

 

1-1-2) cookie와 session의 차이점
"cookie"와 "session" 모두 브라우저가 서버에 요청 시 request message에 cookie를 담아 보내는 것은 동일하다!


"session"의 경우, 서버 측에서 모든 로그인의 부담을 짊어져야(서버의 하드 디스크의 부담이 증가함) 하는

반면, 브라우저는 상대적으로 부하가 적다!

--> 이에 서버를 늘려서 요청을 분산하는 방식을 사용한다!
--> 즉, 서버 측의 부하가 심해진다는 것이 session 방식의 단점이다!

"cookie" 방식은 서버 측의 부하가 session 방식에 비해 많이 줄어든다는 장점이 있다!
서버가 늘어나더라도 cookie 방식은 동작하는 데에 아무런 상관이 없는 반면,

session 방식은 서버가 새로 늘어나면 해당 서버에 내용을 저장할 공간을 새로 생성해 주어야 한다는 차이가 있다!

 

하지만 이는 "cookie"에 여러가지 내용이 포함된다는 단점이 있는데

반해 session은 식별자만 주면 된다는 차이가 있다!
--> 따라서 cookie 방식을 사용할 경우, 요청에 대한 부하는 서버 측에서 늘어날 수 있다!

 

 

 

1-1-3) JWT 개념
로그인 기능을 보다 좋게 만들 수 없을까해서 나온 것이 "JWT(JSON Web Token)"이다!

로그인에 대한 token의 모양을 규격화하자고 해서 등장한 것이 "JWT"이다!
즉, cookie에 데이터를 저장할 시 데이터의 모양이 규격화되도록 한 것이 "JWT"이다!

 

  • JWT는 cookie이다!(즉, JWT는 cookie에 저장되어야 한다!)
  • JWT는 규격이 있다!

그럼 어느 서버에 요청을 보내더라도 필요한 내용을 뽑아 쓸 수 있다!

 

 


1-1-4) 암호화
평문: 사람이 알아볼 수 있는 혹은 읽을 수 있는 문자를 의미함
네트워크 상에서 중간에 다른 사람이 탈취하더라도 그 내용을 이해할 수 없도록 하기 위해 "암호화"가 필요하다!

복호화는 암호화된 것을 다시 평문으로 만드는 것이다!

 

암호화는 크게 단방향과 양방향이 있고,
여기서 양방향은 대칭키와 비대칭키로 구분되며,
단방향에는 hash가 있다!

 

- 단방향: 한 번 암호화를 진행하면 복호화가 안 되는 것을 의미함!

ex) 요즘 사이트에서는 단방향 방식으로 패스워드를 만들기에

만일 사용자가 비밀번호를 잊어버린다면 패스워드를 새로 만들도록 함

--> 이는 기존 비밀번호를 알 수 없어서 새로 비밀번호를 만들도록 하기 때문임!
--> 따라서 단방향 암호화가 보안이 강하다!

 

- 양방향: 암호화를 진행한 것에 대해 다시 복호화가 가능함!


- 대칭키: 평문(ex. 1234, 메뉴명 등)을 암호화하여 보내려는 사람은 특정 key 값을 이용하여 평문을 암호화하여 보내고,

이를 받은 사람은 보낸 사람과 동일한 key 값을 가지고 암호화 된 것을 복호화하여 그 내용(평문)을 확인할 수 있음!

- 비대칭키: 보내는 사람과 받는 사람이 가진 key 값이 서로 다르지만 암호화한 내용을 확인할 수 있도록 하는 방식

 

 

"JWT"는 단방향 암호화 방식(복호화가 안 되는 방식)을 사용한다!
"signature"는 "header"와 "payload"의 2개 string 값을 연결하여 암호화를 진행한다!


암호화의 경우, 동일한 내용을 암호화한다면 동일한 암호화 결과가 나온다!

--> 즉, DB에 저장되어 있는 암호화 값과 본인이 입력한 패스워드의 암호화 값이 일치하는지 비교한 뒤

서로 일치하면 로그인을 허용하는 것이 로그인 기능의 기본 로직이다!

"signature"를 통해 token 값이 바뀌었는지 아닌지를 체크할 수 있다!

참고로 JWT는 이미 라이브러리로 구현되어 있다!

 


1-1-5) JWT 규격 정리

JWT의 규격
"JWT"는 기본적으로 세 가지 형태(headerpayloadsignature)로 구현되어 있다!


2가지 인코딩된 값을 기준으로 암호화를 진행한 것이 "signature"이다!

--> 이는 우선 header, payload 각각에 대해 "Base64 인코딩"을 진행한 뒤

이 2개를 "."으로 연결하여 암호화를 진행한 것이 signature라는 의미이다!

 

 


header.payload.signature

header + payload = signature
--> header + payload = 평문
--> 위의 평문을 가지고 "단방향 암호화"를 진행하여 만든 것이 "signature"이다!

 

  • header : Object(객체)이며, 이를 "string" 타입으로 만들어야 함!
  • payload : Object(객체)이며, 이를 "string" 타입으로 만들어야 함!

Base64 인코딩 : 기본적으로 64진수를 말하며, 이는 컴퓨터가 이해할 수 있는 용어인 64진수로 바꿔주는데

이때 최대한 압축하는 것을 의미함!

컴퓨터는 2진수를 사용하며, 2진수 다음으로 컴퓨터와 친화적인 진수가 16진수이고, 16진수에서 변환하기 좋은 것이 64진수이다!

 

 

 

 

 

1-2) JWT 개념 이해 관련 코드

base64.js(Base64 개념 관련 코드)

const crypto = require("crypto");

const str = "Hello World";

// Buffer: Node.js의 내장 객체
const buf = Buffer.from(str);
// console.log(buf);
// console.log(buf.toString()); // Hello World(Buffer 형태에서 원본 데이터로 돌아옴!)
// console.log(buf.toString("hex")); // 16진수 값으로 바꿔줌!
// console.log(buf.toString("base64")); // 64진수 값으로 바꿔줌!

// 1 byte = 8 bit
// base64는 기본적으로 6 bit씩 짤라서 처리하기에 나머지가 생기는데 이 나머지 값은 "="으로 표현함("="은 빈 공간을 채운 string이기에 날려도 됨!) --> ex) 32 bit / 6 bit => 2 bit가 나머지로 남기에 뒤에 "="이 붙음!

const header = {
  alg: "HS256",
  typ: "JWT",
};

const headerString = JSON.stringify(header);
// console.log(headerString);

const buf2 = Buffer.from(headerString).toString("base64");
// console.log(buf2); // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

// Buffer의 두번째 인자값에는 첫번째 인자값에 들어간 값이 아스키 코드인지 base64인지 알 수 있게 해당하는 인코딩 값을 넣어줘야 함!
const json = Buffer.from(
  "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
  "base64"
).toString("utf-8");
// console.log(json);

// 암호화는 라이브러리가 많은데 그 중 우리가 사용할 암호화는 단방향 암호화이다!
// 단방향 암호화 구현을 위해 사용할 것이 "SHA"인데 단방향 암호화의 특징은 어떠한 데이터를 넣어도 32 byte(64글자를 의미함)가 나온다는 점이다!
// 1 byte === 8 bit === 2 Nibble
// Node.js에서 기본적으로 제공해주는 내장 라이브러리인 "crypto"를 사용함!

const salt = process.env.SALT || "web7722";
// "createHmac("sha256", salt)"은 암호화할 내용에 "salt"를 추가하고 "sha256"으로 암호화를 진행한다는 의미이며(즉, salt 값을 모르면 동일하게 암호화를 진행할 수 없기에 "salt"는 절대로 공개해서는 안 되는 값이다!)
// update는 암호화를 진행할 평문 값을 넣는 공간이며, digest는 결과물에 대해 어떤 인코딩을 할지 지정하는 곳이다!
const hash = crypto.createHmac("sha256", salt).update(buf2).digest("base64");
console.log(hash);
console.log(hash.length);

// const hashBuf = Buffer.from(hash).toString("base64");
// console.log(hashBuf);

 

 

jwt_test.js(JWT 개념 관련 코드)

const crypto = require("crypto");

const header = {
  alg: "HS256",
  typ: "JWT",
};

const payload = {
  sub: "1234567890",
  userid: "admin",
  iat: 1516239022,
};

function encode(obj) {
  return Buffer.from(JSON.stringify(obj)).toString("base64");
}

const header64 = encode(header);
const payload64 = encode(payload);
// console.log(header64, payload64);

const 평문 = header64 + "." + payload64;
// console.log(평문);

const signature = crypto
  .createHmac("sha256", "web7722")
  .update(평문)
  .digest("base64url");
console.log(signature);

 

 

lib/jwt.js(JWT 개념을 class를 활용해 표현한 코드)

const crypto = require("crypto");

class JWT {
  constructor({ crypto }) {
    this.crypto = crypto;
  }

  sign(data, options = {}) {
    const header = this.encode({ tpy: "JWT", alg: "HS256" }); //base64url
    const payload = this.encode({ ...data, ...options }); //base64url
    const signature = this.createSignature([header, payload]);

    // return `${header}.${payload}.${signature}`;
    return [header, payload, signature].join(".");
  }

  // token: string, salt: string
  verify(token, salt) {
    // eyJ0cHkiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyaWQiOiJ3ZWI3NzIyIiwidXNlcm5hbWUiOiJpbmdvbyJ9.uMiI-vrtl0X_u2hg64YZGCOvvlogEYBOwradyX6duyU
    // header, payload --> hash 진행
    // 기존 hash와 새로운 hash를 비교하여 true인지 아닌지 체크
    const [header, payload, signature] = token.split(".");
    const newSignature = this.createSignature([header, payload], salt);
    if (newSignature !== signature) {
      throw new Error("토큰이 이상함! 누가 변조한 것 같음!");
    }

    return this.decode(payload);
  }

  encode(obj) {
    return Buffer.from(JSON.stringify(obj)).toString("base64url");
  }

  decode(base64) {
    return JSON.parse(Buffer.from(base64, "base64").toString("utf-8")); // 객체를 반환함!
  }

  createSignature(base64urls, salt = "web7722") {
    // header.payload .join
    const data = base64urls.join(".");
    return this.crypto
      .createHmac("sha256", salt)
      .update(data)
      .digest("base64url");
  }
}

const jwt = new JWT({ crypto });

const token = jwt.sign({ userid: "web7722", username: "ingoo" }); // JWT
console.log(token); // eyJ0cHkiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyaWQiOiJ3ZWI3NzIyIiwidXNlcm5hbWUiOiJpbmdvbyJ9.uMiI-vrtl0X_u2hg64YZGCOvvlogEYBOwradyX6duyU

const payload = jwt.verify(
  "eyJ0cHkiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyaWQiOiJ3ZWI3NzIyIiwidXNlcm5hbWUiOiJpbmdvbyJ9.uMiI-vrtl0X_u2hg64YZGCOvvlogEYBOwradyX6duyU",
  "web7722"
);
console.log(payload); // { userid: 'web7722', username: 'ingoo' }