0. 앞선글

 

React를 사용해 로그인 기능을 구현하던 도중 예제에서 JWT를 통해 인증 기능을 구현한 글을 봤습니다. JWT가 좋다 많이 쓴다 하지만 막연히 인증을 위해 쓰면 된다고만 알고 있었을 뿐 대체 왜 쓰는지 알아보지 않았습니다. 이 기회에 대체 요즘의 웹에서는 왜 JWT를 사용하는가에 대해 알아보고자 합니다.

 

이 글은 Mariano Calandra님께서 hackernoon에 작성한 글 중 하나를 번역 한 글이며 원문은 다음과 같습니다.

Why do we Need the JSON Web Token (JWT) in the Modern Web Era?

 

Why do we Need the JSON Web Token (JWT) in the Modern Web Era? | Hacker Noon

Hold on tight: the HTTP protocol is terribly flawed(*) and when it comes to user authentication this problem screams loudly.

hackernoon.com

 

 

 

1. 왜 모던 웹 시대에 JWT(JSON Web Token)이 필요할까?

 

 

HTTP 프로토콜은 엄청난 결함*이 있으며 사용자 인증에 관해서는 이 문제가 크게 작용합니다.

 

오랜 기간 동안 우리는 개발자로서 HTTP 프로토콜로 싸워왔습니다. 때로는 좋은 결과를 얻었고 그렇지 않을 때도 있지만 우리는 행복하다고 생각했습니다. 불행히도 웹은 빠르게 발전되었고 이러한 솔루션 중 많은 부분이 너무 빨리 구식이 되었습니다.

 

망설이는 자는 길을 잃습니다...

 

시간이 지나고 한 무리의 사람들이 "문제"와의 싸움을 그만두고 이제 그것을 받아들여야 할 때라는 것을 깨달았습니다. 그로 인해 탄생한 결과를 JSON Web Token(JWT)이라고 하며 여기에서 이야기를 들려 드리도록 하겠습니다.

 

 

* HTTP의 Stateless 특성은 분명히 결함은 아닙니다. 단지 도발이었습니다 :)

 

 

 

2. 옛날 옛적에...

 

REST API(예를 들어 GET / orders)가 있고 권한이 있는 사용자에게만 액세스를 제한하려고 한다고 가정해 봅시다.

 

가장 단순한 접근 방식은 API가 사용자 이름과 비밀번호를 요청하는 것입니다. 그리고 해당 자격 증명이 실제로 존재하는지를 데이터베이스에서 검색해 인증을 확인합니다. 마지막으로 인증된 사용자에게 해당 요청을 수행할 권한이 있는지 확인합니다. 두 검사 모두 통과하면 실제 API가 실행됩니다. 논리적인 것 같아 보입니다.

 

 

 

3. State에 대한 문제.

 

HTTP 프로토콜은 Stateless로 동작합니다. 새 요청(예를 들어 GET / order / 42)은 이전 요청에 대해 아무것도 알지 못하기 때문에 새 요청마다 다시 인증해야 합니다 (그림 1).

 

[그림 1 ] HTTP 프로토콜의 Stateless 특성으로 인해 새로운 모든 API 요청에는 완전한 인증이 필요합니다.

 

이를 처리하는 전통적인 방법은 SSS(Server Side Sessions)를 사용하는 것입니다. 이 시나리오를 통해 보자면 먼저 사용자 이름과 비밀번호를 확인합니다. 이들이 인증된 경우, 서버는 세션 ID를 메모리에 저장하고 그 ID를 클라이언트에게 반환합니다. 그 후부터는 클라이언트를 식별하기 위해 세션 ID를 서버로 보내면 됩니다 (그림 2).

 

[그림 2] SSS를 사용하면 인증 데이터베이스에 대한 인증요청 수가 줄어듭니다.

 

이 솔루션은 문제를 해결하지만 다른 문제를 만듭니다. 아마 더 큰 문제를 말입니다.

 

 

 

4. 확장에 대한 문제.

 

IT 세상에서의 시간은 빨리 흐릅니다. 어제 일반적으로 사용되었던 솔루션이 구식이 되었을 수도 있습니다. SSS도 그중 하나입니다.

 

API 시대에는 우리의 엔드 포인트가 많은 요청에 직면할 수 있으므로 인프라의 확장이 필요합니다. 스케일링에는 두 가지 유형이 있습니다.

  • 수직 확장: 서버에 더 많은 리소스를 추가하는 것을 의미합니다. 이는 한계점이 낮은 비싼 솔루션입니다. (예를 들면 서버에 최대 리소스를 할당하는 것)
  • 수평 확장: 로드 밸런서 뒤에 새 서버를 추가하는 것을 의미합니다. 이는 더 간단하고 비용면에서 효과적입니다.

두 번째 접근이 유리하단 것이 명확해 보입니다. 어떤 일이 일어날지 한번 살펴보겠습니다.

 

초기 시나리오에서는 로드 밸런서 뒤에 서버가 하나만 있습니다. 클라이언트가 세션 ID xyz를 사용하여 요청을 수행하면 해당 레코드는 서버의 메모리에서 찾을 수 있다는 것이 보장됩니다. (그림 3).

 

[그림 3] 로드 밸런서 뒤에있는 단일 서버. 요청의 세션 ID는 서버 메모리에서 찾을 수 있습니다.

 

여태까지는 그런대로 잘 되어가고 있습니다. 이제 위의 인프라를 확장해야 한다고 상상해 봅시다. 새로운 서버 (예를 들어 서버 2:2)가 로드 밸런서 뒤에 추가되며 추가된 새로운 서버는 xyz 클라이언트가 발행 한 요청을 다음과 같이 처리합니다(그림 4).

 

[그림 4] 새 서버가 LB 뒤에 있으며 이전 세션에 대해 아무것도 모르므로 사용자가 인식되지 않습니다.

 

인증되지 않습니다! 새로운 서버의 메모리에는 xyz 세션이 없으므로 인증 프로세스가 실패합니다. 이 문제를 해결하기 위해 사용할 수 있는 세 가지 해결 방법이 있습니다.

  • 서버 간 세션 동기화: 구현에 까다롭고 오류가 발생하기 쉽습니다.
  • 외부 인메모리 DB 사용: 좋은 솔루션이지만 다른 인프라 구성요소가 추가됩니다.
  • 다른 세 번째 방법: HTTP의 Stateless 특성을 수용하고 더 나은 솔루션을 찾습니다.

 

 

 

5. 더 나은 해결책.

 

JWT(JSON Web Token)는 공개 표준(RFC 7519)으로, 인증 및 권한 부여 여부와 같은 두 당사자(발급자 및 대상) 간에 정보를 전송하는 방법을 정의합니다. 발행된 각 토큰은 디지털 서명되어 안전한 통신이 이루어 지므로 사용자는 토큰이 진짜인지 또는 위조되었는지를 확인할 수 있습니다.


각 토큰은 API에 대해 주어진 요청을 허용하거나 거부하는데 필요한 모든 정보가 자체 포함되어 있습니다. 토큰을 확인하는 방법과 권한 인증이 발생하는 방법을 이해하려면 한 발짝 물러서서 JWT를 살펴봐야 합니다.

 

 

6. JWT 해부.

 

JWT 토큰은 본질적으로 인코딩 된 긴 텍스트 문자열입니다. 이 문자열은 점(.) 기호로 구분된 세 개의 작은 부분으로 구성됩니다. 이 부분들은 다음과 같습니다.

  • 헤더(header)
  • 페이로드(payload) 또는 바디(body)
  • 서명(signature)

따라서 토큰은 다음과 같습니다.

 

header.payload.signature

 

6.1. 헤더(header)

 

헤더 섹션에는 토큰 자체에 대한 정보가 포함되어 있습니다.

 

{
  "kid": "ywdoAL4WL...rV4InvRo=",
  "alg": "RS256"
}

 

이 JSON은 토큰(alg)을 서명하는 데 사용된 알고리즘과 유효성을 검사하는 데 사용해야 하는 키(kid)가 무엇인지 설명합니다. 이 JSON은 최종적으로 Base64 URL로 인코딩 됩니다. 

 

eyJraWQiOiJ -TRUNCATED- JTMjU2In0

 

6.2. 페이로드(payload) 또는 바디(body)

 

페이로드는 JWT의 가장 중요한 부분입니다. 클라이언트에 대한 정보(JWT의 클레임들)가 포함되어 있습니다.

 

{
  [...]
  "iss": "https://cognito-idp.eu-west-1.amazonaws.com/XXX",
  "name": "Mariano Calandra",
  "admin": false
}

 

iss 속성은 등록된 클레임이며 토큰을 발급 한 자격 증명 공급자 (이 경우 Amazon Cognito)를 나타냅니다. 필요에 따라 클레임을 추가할 수 있습니다(예를 들어 admin 클레임). 그런 다음 페이로드는 Base64 URL로 인코딩 됩니다.

 

eyJzdWIiOiJkZGU5N2Y0ZC0wNmQyLTQwZjEtYWJkNi0xZWRhODM1YzExM2UiLCJhdWQiOiI3c2Jzamh -TRUNCATED- hbnRfaWQiOiJ4cGVwcGVycy5jb20iLCJleHAiOjE1N jY4MzQwMDgsImlhdCI6MTU2NjgzMDQwOH0

 

6.3. 서명(signature)

 

토큰의 세 번째 부분은 다음 단계에 따라 계산된 해시입니다.

  • 인코딩 된 헤더와 인코딩 된 페이로드를 점(.)으로 합칩니다.
  • 헤더의 alg 속성(이 경우는 RS256)과 개인키에 지정된 암호화 알고리즘을 사용해 결괏값을 해시합니다.
  • 해시된 결과를 Base64 URL로 인코딩합니다.

의사 코드(pseudo-code)를 통해 보도록 합시다.

 

data = base64UrlEncode(header) + "." + base64UrlEncode(payload);
hash = RS256(data, private_key);
signature = base64UrlEncode(hash);

 

이 내용을 계산하면 다음과 같은 서명이 나옵니다.

 

POstGetfAytaZS82wHcjoTyoqhMyxXiWdR7Nn7A29DNSl0EiXLdwJ6xC6AfgZWF1bOsS_TuYI3OG85 -TRUNCATED- FfEbLxtF2pZS6YC1aSfLQxeNe8djT9YjpvRZA

 

6.4 모든 걸 합쳐서.

 

인코딩 된 헤더, 인코딩 된 페이로드, 인코딩 된 서명이 있으면 조각을 간단히 점에 의한 병합으로 모든 것을 결합할 수 있습니다.

 

eyJzdWIiOiJkZGU5N2Y0ZC0wNmQyLTQwZjEtYWJkNi0xZWRhODM1YzExM2UiLCJhdWQiOiI3c2Jzamh -TRUNCATED- hbnRfaWQiOiJ4cGVwcGVycy5jb20iLCJleHAiOjE1N jY4MzQwMDgsImlhdCI6MTU2NjgzMDQwOH0.eyJzdWIiOiJkZGU5N2Y0ZC0wNmQyLTQwZjEtYWJkNi0xZWRhODM1YzExM2UiLCJhdWQiOiI3c2Jzamh -TRUNCATED- hbnRfaWQiOiJ4cGVwcGVycy5jb20iLCJleHAiOjE1N jY4MzQwMDgsImlhdCI6MTU2NjgzMDQwOH0.POstGetfAytaZS82wHcjoTyoqhMyxXiWdR7Nn7A29DNSl0EiXLdwJ6xC6AfgZWF1bOsS_TuYI3OG85 -TRUNCATED- FfEbLxtF2pZS6YC1aSfLQxeNe8djT9YjpvRZA

 

참고: 위의 토큰이 암호화된 것처럼 보이지만 그렇지 않습니다! RS256과 달리 Base64 URL은 암호화 알고리즘이 아니므로 페이로드를 염두에 두십시오!

 

6.5. JWT 검증

 

토큰은 자체 포함되어 있으므로 검증에 필요한 모든 정보를 갖고 있습니다. 예를 들어, RS256(헤더의 alg 속성)과 개인 키를 사용하여 토큰이 서명되었음을 알 수 있습니다. 이제 검증을 수행하기 위해 올바른 공개 키를 얻는 방법을 알아야 합니다. 네 공개키 말입니다.

 

참고 : 비대칭 암호화에서 공개 키는 메시지를 암호화하는 데 사용되고 개인 키는 메시지를 해독하는 데 사용됩니다. 서명 알고리즘에서 이 프로세스는 완전히 전환되어 있습니다! 여기서 메시지(위의 의사 코드의 데이터)는 개인 키를 사용하여 서명되며 공개 키는 서명이 유효한지 확인하는 데 사용됩니다.

 

본문의 iss 속성은 발급자의 엔드포인트(이 경우 Amazon Cognito이지만 다른 공급자와 크게 다르지 않아야 합니다.)를 나타내며 해당 URI를 복사하여 문자열 /.well-known/jwks.json에 추가합니다. 아마 다음과 같이 보일 것입니다.

 

https://cognito-idp.eu-west-1.amazonaws.com/XXX/.well-known/jwks.json

 

위의 URL 다음에 JSON이 위치합니다.

 

{
  "keys": [
    {
      "alg": "RS256",
      "e": "AQAB",
      "kid": "ywdoAL4WL...rV4InvRo=",
      "kty": "RSA",
      "n": "m7uImGR -TRUNCATED AhaabmiCq5WMQ",
      "use": "sig"
    },
    {...}
  ]
}

 

키 배열에서 동일한 토큰 헤더의 kid가 있는지 검색하세요. 속성 e와 n은 공개 키를 계산하는 공개된 설명자와 와 절댓값입니다.


우리가 그것을 얻으면 서명을 확인할 수 있습니다. 만약 유효하다면 토큰에 포함된 정보를 신뢰할 수 있습니다.

참고 : 공개 키 계산 또는 서명 확인 프로세스는 쉽지 않으며이 게시물의 범위를 벗어납니다.

 

 

 

7. 실제 시나리오.

 

처음 액세스 할 때 클라이언트는 권한 부여 서버(여기서는 Amazon Cognito, Microsoft, Salesforce 또는 유사한 다른 공급자)에 연결하여 사용자 이름과 암호를 보내야 합니다. 자격 증명이 유효하면 JWT 토큰이 클라이언트에 반환되고 이를 사용하여 API를 요청합니다(이 예제에서는 Amazon API Gateway 엔드 포인트).

 

[그림 5] 실제 시나리오의 전체 흐름.


위 시나리오(그림 5)에서 API는 토큰 유효성 검사의 유일한 책임자이며 서명이 위조된 경우 요청을 거부할 수 있습니다.

 

더 나아 가 클라이언트가 보호된 API를 호출하여 주문을 삭제하려고 한다고 가정하고(예를 들어 DELETE / order / 42) 이 조치는 관리자만 수행해야 한다고 합시다.
JWT를 사용하면 페이로드 본문에 사용자 지정 클레임을 추가하는 것만큼만 이 작업을 수행하기가 어려울 뿐입니다(예를 들어 위 페이로드의 클레임인 admin: true). API가 호출되면 먼저 서명 인증을 확인한 후 admin 클레임이 참인지 확인합니다.

 

 

 

8. 정리.

 

지금은 여기까지입니다. 우리는 JWT에 대해 많은 것을 보았지만 여전히 다른 것이 빠져 있습니다.

  • JWT를 얻도록 Amazpn Cognito를 어떻게 구성하나요?
  • 사용자 지정 클레임을 추가할 수 있도록 Amazon Cognito를 어떻게 구성하나요?
  • 인증을 위한 JWT를 프로그래밍적으로 검증하는 방법은 무엇입니까?

 

걱정하지 마세요. 나중에 이 질문에 대답할 기회가 있을 겁니다. 지금은 몇 가지 핵심사항에 대해 요약을 해보도록 하겠습니다.

  • HTTP 프로토콜은 Stateless이므로 새 요청은 이전 요청에 대해 아무것도 알지 못합니다.
  • SSS는 HTTP의 Stateless에 대한 솔루션 이었지만 장기적으로 보았을 때 확정성에 위협이 되었습니다.
  • JWT는 API에 대한 요청을 허가하나 거부하는데 필요한 모든 정보가 자체 포함되어 있습니다.
  • JWT는 설계상 Stateless이므로 HTTP의 Stateless와 싸우지 않아도 됩니다.
  • JWT는 인코딩 되어있지 암호화되어있는 것이 아닙니다.

 

 

9. 맺음글.

 

굉장히 유용한 글이었습니다. JWT가 왜 나타나게 되었는지부터 설명되어 있어 자연스레 JWT의 필요성을 알게 해 주는 글입니다.

 

단순히 글로만 설명하는 것이 아닌 그림으로 워크플로우를 알려주고 있어 이해하기 편했습니다. 또한 JWT의 기본적인 구조와 검증 원리 역시 설명하고 있어 JWT를 이해하는데 많은 도움이 되었습니다. 

 

여러분들도 읽고 많은 도움이 되셨으면 좋겠습니다.

 

 

 

 

 

반응형

+ Recent posts