본문 바로가기
개발공부일지/NodeJS

NodeJS - JWT (JSON Web Token) 기초 및 개념 이해하기

by Hynn1429 2023. 1. 17.
반응형

안녕하세요.
Hynn 입니다.

이번 포스팅에서는 로그인 기능을 완전히 구현하기 위해 JWT 에 대해서 학습하고자 합니다.
이전의 미니 프로젝트에서는 Token 을 이용해서 로그인 기능을 구현했었습니다.
하지만 실제 Token 을 그대로 사용하기에는, 보안상의 취약점이 분명히 존재합니다.

이를 해소하기 위한 JWT 기능에 대해서 알아보기

==========

1. JWT 개념 이해하기

2. JWT 기본 적용사항 알아보기

3. 실제 코드 예제 작성해보기

==========

1. JMT 개념 이해하기

JMT, 즉 JSON Web Token 은, 기존의 Cookie, Session 같은 기술을 대체하는 새로운 기술은 아닙니다.
기존의 HTTP통신의 4way Hand-shake 의 통신은 TCP 통신에서 3way hand-shake 방식에서 연결이 종료되는 개념이 추가되는 개념이 전달이 되고,
실제 HTTP 통신은 한차례 통신 단계를 거치면 연결을 종료함으로서 4way Hand-shake 가 종료됩니다.

이를 로그인같은 정보에 구현하기 위해 사용하는것이, Cookie, Session 입니다.
이 두 방식의 차이점은 단 한가지 입니다.
바로 Cookie 는 Client 의 LocalStorage, Session 은 Server 에 저장됩니다.

하지만 이 방식 모두 각각의 단점이 존재합니다.
보안에 다소 취약하기도 하고, Cookie 의 경우, 담고있는 정보의 내용이 일반적으로 적지 않습니다.
그리고 서버는 동시에 사용자가 몰릴 경우 부하가 발생할 수 있습니다.

세션은 서버에 저장되는 개념이기에, 덜할 수 는 있지만, 데이터의 안전성의 대한 위험은 여전히 존재합니다.
이러한 단점들을 개선하기 위해서 추가적으로 사용하는 기술이 바로 JWT 입니다.

이 JWT 역시 기반적인 부분은 Cookie,Session 과 다르지는 않습니다.

하지만 어떻게 다른것인지 살펴보도록 하겠습니다.

JWT 는 말그대로 JSON Object 형태로 안전하게 데이터를 전송하기 위한 개방형 표준입니다.
JWT 를 활용하는 서비스는 다양하게 존재할 수 있지만, 가장 대표적으로 사용하는 2개의 서비스는 아래와 같습니다.

  • 권한 부여
  • JWT를 사용하면, 사용자가 웹 서비스에 로그인을 시도할 때, 요청한 정보를 JSON Object 화하여, Token 으로 허용될 경우, 서비스 및 리소스에 액세스할 수 있습니다. 이 기능은 흔히 SSO(Single-Sign-On) 기능으로도 불리우며, 오버헤드의 부담이 적고, 다양한 도메인에서 손쉽게 사용할 수 있습니다.
  • 정보 교환
  • 역시 당사자 간의 정보를 안전하게 전송하는 좋은 방법입니다. 당사자간의 전송/수신 시 공개/개인 키 쌍을 사용하여 JWT 에 서명이 가능할 뿐 아니라, Header,Payload 를 사용하여 서명을 계산하기 때문에 컨텐츠가 변조되지 않았음을 확인할 수도 있습니다.

JWT는 3가지의 요소로 구성되어 있습니다.
3가지의 구성요소를 이해한다면 손쉽게 JWT의 역활을 이해할 수 있습니다.

  • Header
  • Payload
  • Signature

JWT 공식 홈페이지에서 보시다시피, 3개의 구성요소가 되어 있고, 그 각 구성요소를 Encode, Decode 하는 구성요소의 예시를 보여주고 있습니다.
이 암호화에 대해서 간략하게 정의하면 아래와 같이 설명할 수 있습니다.

Header, Payload 는 흔히 말하는 평문, 즉 암호화가 되기 전 메시지를 나타냅니다. 이 2개의 단계까지는 이전의 우리가 알고 있는 Cookie, Sesison 과 크게 다르지 않아 보일겁니다.
이를 이제 Signature, 즉 서명 기능이 들어가면서, 암호화가 이루어집니다.
이 암호화 방식은 "Base64 URL Safe" 라는 방식으로 암호화가 이루어집니다.

즉 각각의 정보에 어떤것이 담기는지를 알아야 합니다.
공식 홈페이지의 예제를 가지고 설명드리도록 하겠습니다.

Header
{
"alg": "HS256",
"typ": "JWT"
}

Header 에는 2개의 구성요소가 예제로 나타나 있습니다.

각각의 요소는 아래를 뜻합니다.

  • Alg (Algorithm) - 서명시 사용되는 알고리즘을 뜻합니다.
  • Typ (Token Type) - 서명시 사용하는 Token 을 식별하는 타입을 지정합니다.

즉 Header 의 영역에는 암호화를 할 알고리즘과, 토큰을 식별하는 타입을 지정합니다.

즉 실제적인 데이터가 담기는 영역은 아닙니다.

하지만, 이러한 방식을 다른 사람이 알면 안될 정보기 때문에, 이전 포스팅에서 다루었던, ".env" 와 같은 형태로 변수를 담아 작성하는 것이 좋습니다.

Payload
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

실제적인 데이터가 들어가는 영역입니다. 이전에는 "req.body" 라는 형태의 이름으로 많이 접해보았을 사항이기도 합니다.

즉, 이 Payload 영역에는 사용자가 로그인을 시도한다면, userID, userPW 같은 정보들이 담기게 될 것입니다.

이 영역에는 일정한 규칙, 즉 Key: Value 의 형태로 값이 담기게 됩니다. 당연하게도 Object 일 것입니다.

Signature
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),

your-256-bit-secret

) secret base64 encoded

Signature 는 예제 코드에서도 나타난것 처럼, Header,Payload 라는 암호화되지 않은 "평문" 을 서명한 값입니다.

이를 단계에 나누어 표현하면 아래의 과정을 거치게 됩니다.

1. Header 의 Alg 에 정의된 알고리즘과 Typ 를 이용하여 Secret Key 를 생성합니다.

2. 그리고 "HMACSHA256" 의 명시된 "Base64URL" 방식을 사용하여 Encoding 을 진행합니다.

3. Header,Payload,Signature 가 암호화 된 값으로 아래와 같은 예제처럼 암호화가 됩니다.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

이제 서버는 위 키를 Header 의 속성과 공개 키를 이용해 검증이 가능합니다.

하지만 알아야 할 사항은, SSO (SingleSignOn) 이라는 방식의 로그인은, 비대칭 암호화라는 방식을 사용합니다.

즉, 위의 방식을 설명하면, 공개키를 이용해 암호화 한 값은, 공개키를 사용하여 복호화가 되지 않습니다.

일반적으로 과거의 웹 사이트를 이용해보신분들이라면, "비밀번호 찾기" 를 사용했을 시, 과거에는 비밀번호를 알려주었다면, 최근의 웹 사이트들은 비밀번호를 알려주지 않고 새로운 암호를 생성하도록 합니다.

즉 복호화를 할 수 없기 때문입니다.

물론 비대칭 암호화 방식의 경우, "Secret Key" 를 사용하여 복호화 할 수 있기는 하지만, 당연히 이 키는 공개되지 않는 값입니다.
이제 JWT 의 대한 기초적인 Code 를 사용해서 어떠한 과정을 거치는 지 알아보도록 하겠습니다.

먼저 JWT 를 사용하기 위해 "Crypto" 라는 Module 을 설치하고, 이를 Require 를 이용해 가져오도록 하겠습니다.

 

NPM Install crypto
const crypto = require("crypto")

이제 각각의 변수 선언을 통해, Header, Payload 를 변수에 담아 보도록 하겠습니다.

 

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

const payload = {
    userId : "1234567890",
    userPw : "1234567890",
}

 

이제 이를 예제의 방식처럼 암호화를 시도해보도록 하겠습니다.

단계별로 표현하면 아래와 같습니다.

 

const headerString = JSON.Stringify(header)
consloe.log(headerString)

const buf = Buffer.from(headerString).toString("base64")
console.log(buf)

const json = Buffer.from("EncryptoValue").toString("utf8")
console.log(json)

먼저 첫번째 변수에는, Header 의 값을 String 으로 변환하였고, 아래와 같은 결과물이 나타날 것입니다.

{"alg":"HS256","type":"JWT"}

이를 내장 객체를 이용해서,  base64로 Encoding 한 값이 됩니다.

eyJhbGciOiJIUzI1NiIsInR5cGUiOiJKV1QifQ==

그리고 이를 다시 "utf8" 로 변환한 값을 출력하였습니다.

{"alg":"HS256","type":"JWT"}

즉 서버에서는 이러한 과정을 거친다고 생각하시면 이해하기 쉽습니다.

물론 JWT 에서는 "Base64URL" 을 사용하므로, 결과가 다소 다를 수 있습니다. 바로 "==" 이라는 끝 부분입니다.

bit 단위로 표시되는 암호화 값은, 비트(Bit) 단위로 계산되는 값이 남을 경우 표시되는 값입니다. 즉 위의 예시에서는 "2bit" 가 남는 것이죠.

이를 Base64URL 로 변환하면, 이러한 처리도 되므로, 아래와 같은 값이 출력 될 것입니다.

eyJhbGciOiJIUzI1NiIsInR5cGUiOiJKV1QifQ

 

기초적인 부분을 보았으니, 실제 JavaScript 로 한번 작성해보겠습니다.

 

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
    verify(token, salt) {
        const [header, payload, signature] = token.split(".")
        const newSignature = this.createSignature([header, payload], salt)
        if (newSignature !== signature) {
            throw new Error("Invalid")
        }

        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 = "tistory") {
        // 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: "Hynn", username: "hyunsign" }) // JWT
console.log(token)
// Header.Payload.Signature

const payload = jwt.verify("Header.Payload.Signature", "hynn")
console.log(payload)

위의 예제 코드를 가지고 이해를 돕기 위해 해설을 같이 드리자면 아래와 같습니다.

먼저 위 방식은, 이전 포스팅의 연장선에서 "Class" 생성자 함수를 사용해서 "Template" 를 구현했습니다.

위 코드를 실제 파일에 적용해서 테스트 해보시는 것을 권장드립니다.

 

감사합니다.

반응형

댓글