안녕하세요.
Hynn 입니다.
이번 포스팅에서는 로그인 기능을 완전히 구현하기 위해 JWT 에 대해서 학습하고자 합니다.
이전의 미니 프로젝트에서는 Token 을 이용해서 로그인 기능을 구현했었습니다.
하지만 실제 Token 을 그대로 사용하기에는, 보안상의 취약점이 분명히 존재합니다.
이를 해소하기 위한 JWT 기능에 대해서 알아보기
==========
==========
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 를 사용해서 어떠한 과정을 거치는 지 알아보도록 하겠습니다.
2. JMT 개념 이해하기
먼저 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 로 한번 작성해보겠습니다.
3. JWT 예제 작성해보기
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" 를 구현했습니다.
위 코드를 실제 파일에 적용해서 테스트 해보시는 것을 권장드립니다.
감사합니다.
'개발공부일지 > NodeJS' 카테고리의 다른 글
TDD(Test-Driven-Development) 기본 이해하기 (0) | 2023.01.19 |
---|---|
NodeJS - JWT Login Back-end Example (0) | 2023.01.19 |
NodeJs - ORM을 이용해서 backend 데이터 사용 예제 (Feat. Sequelize) (0) | 2023.01.16 |
NodeJS - .ENV 를 이용해 환경변수 관리하기 (DOTENV) (0) | 2023.01.10 |
NodeJS - Ajax실제 작성예시 따라해보기 (0) | 2023.01.08 |
댓글