JWT ( JSON Web Token )

Kyle·2022년 8월 4일
0
post-custom-banner

참고

인증(Authentication)과 인가(Authorization)

JWT(JSON Web Token)

[Server] JWT(Json Web Token)란?

JWT (JSON Web Token) - 마이크로서비스를 위한 인증과 인가 - Opennaru, Inc.

[JWT] JSON Web Token 소개 및 구조

[JWT] 토큰(Token) 기반 인증에 대한 소개

인증(Authentication) 과 인가(Authorization)

인증 ( Authentication )

  • 사용자가 누구인지 확인하는 절차
  • 회원가입, 로그인 과정이 인증의 대표적인 예

회원가입 과정

  1. id, pw 생성
  2. 비밀번호 암호화 db 저장

로그인 과정

  1. 등록된 id, pw 입력
  2. db에 저장된 데이터와 비교
  3. 일치하면 로그인 / 일치하지않으면 실패
  4. 성공시 Access Token 전송
  5. 최초 로그인 성공 후 다음 접속시에는 Access Token을 이용해 검증하여 접속 유지

Access Token

  • 유효성검증, 보안성까지 고려한 정보 통신이 필요할때 JSON 데이터를 암호화하여 클라이언트와 서버간 정보를 주고받는 JWT를 이용하게 된다.

인가 ( Authorization )

  • 사용자의 요청을 실행할 수 있는지 권한 여부를 체크하는 절차

인가 절차

  1. 인증 절차를 통해 Token을 생성한다. 이토큰은 사용자의 정보를 갖고있다.
  2. request에 Token 을 첨부하여 통신한다.
  3. 서버는 해당 Token을 이용하여 정보를 얻는다.
  4. 정보를 사용하여 DB에서 사용자 권한을 확인한다.
  5. 권한 확인시 요청 처리 / 권한없다면 에러코드 반환

JWT (Json Web Token)

JWT?

jwt란 JSON 포맷을 이용하여 사용자에 대한 속성을 저장하는 claim 기반의 Web Token이다.

JWT의 특징은 가볍고, 토큰 자체에 정보가 포함되어있기 때문에, 정보로 사용하는자가 수용적인 방식으로 정보를 안전하게 전달한다.

Claim 기반이란?

클레임이란 사용자 정보나 데이터속성등을 의미한다.

클레임 기반 토큰은 토큰안에 정보를 담을 수 있는 특징이 있다.

클레임 기반 토큰은 아래와 같이 정보를 담고 있다. jwt는 클레임토큰중 가장 대표적인 것이다.

{
	"id": "kyle1234",
	"username": "kyle"
}

과거 일반토큰 기반은 의미가없는 문자열 기반으로 구성되어있다.

등장배경

  • 세션과 쿠키를 이용하여 인가하는 과정에서 여러 서버가 생겨나면서 발생한 문제에서 생겨나게 됨

마이크로서비스

  • JWT는 마이크로 서비스를 위해 만들어졌다.
  • 기존의 토큰 방식 인증은 모든 서비스에 호출되어 사용된다. 즉, 서버측에서 유저들의 정보를 기억하고 있어야한다.
  • 서비스를 받기 위해 토큰의 유효성을 확인하여 세부정보를 쿼리해야한다. (토큰자체에 내용이 포함되어있지않음)
  • 토큰자체에 내용이 포함되어있지 않기때문에, 항상 상호작용할때마다 다시 접속해야한다.
  • 이러한 방식은 웹/모바일 앱 app 시장이 늘어나게 되면서 서버 확장이 필요하게되면서 많은 문제를 보이게 된다.

서버기반 인증의 문제점

  • 세션
    • 유저가 인증할 때, 서버는 이 기록을 서버에 저장해야한다. 이를 세션이라고 하며 대부분 메모리에 저장이된다. 유저수가 늘어나게되면 서버의 램이 과부하가 된다. 이를 피해 db에 세션을 저장하는 방법도 있지만, 이는 db의 성능에 무리를 줄 수 있다.
  • 확장성
    • 세션을 사용하면 서버를 확장하는 것이 어려워진다.
    • 서버 확장은 단순히 서버의 사양의 업그레이드가아니라, 여러대의 서버를 추가하는 것도 의미한다. 세션을 사용하면서 분산시스템을 설계하는 것이 불가능한 것은 아니지만, 과정이 복잡해서 추천하진 않는다.
  • JWT는 토큰이 필요한 모든 정보를 포함하고 있기떄문에, 참조가 필요없게 되어 마이크로서비스 자체에서 유효성 검증을 할수 있게된다.
  • 특정 서버에 인증에 필요한 정보를 저장해둔 것이 아니라, 필요한 정보를 지닌채 서버에 도달하기 때문에 signature 암호화를 해독할 수 있는 secret key가 공유된 서버라면 토큰을 받은 서버가 검증까지 할 수 있다.
  • 이러한 목적을 위해 JWT는 마이크로 서비스에서 사용되기 시작됐다.

JWT 구조


jwt는 세 파트로 구분되며, 각 파트는 점으로 구분하여 표현된다.

Header와 payload의 값들을 base64로 인코딩하고, 그 값을 비밀키로하여 header에서 정의한 알고리즘으로 해싱, 이 과정에서나온 값을 다시 base64로 인코딩하여 signature부분을 생성한다.

여기서 base64란, 8비트 이진 데이터를 문자 코드에 영향을 받지 않는 공통 ASCII 영역의 문자들로만 이루어진 일련의 문자열로 바꾸는 인코딩 방식을 가린키는 개념이다.

→ 64진법 인코딩, 컴퓨터가 이해할수있도록 인코딩

Header는 typ, alg 두가지 정보를 포함한다.

typ : 토큰의 타입 지정
alg : 해싱 알고리즘 지정

해싱 알고리즘은 보통 HMAC SHA256 / RSA가 사용되며 이 알고리즘은 토큰을 검증할 때 사용되는 signature 부분에서 사용된다.

import json
import base64

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

hd = json.dumps(header).encode("ascii")
hd_encode = base64.b64encode(hd).decode("ascii").replace("=", "")
>> 'eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJIQTI1NiJ9'

base64로 인코딩 시 뒤에 = 문자가 한두개 붙는경우가 있는데, 이 문자는 base64의 padding문자라고 부른다.

JWT토큰은 URL의 파라미터로 전달될 때도 있기때문에, = 문자의 경우는 url-safe하지 않으므로 전부 지워주는것이 좋다. (지워도 디코딩시 문제없음.)

Payload

Payload 부분은 토큰에 담을 정보가 포함된다. 여기에 담는 정보의 한 '조각'을 클레임(Claim)이라고 하며, key value 로 이루어져 있다.

  • 클레임의 종류

    • registered claim (등록된 클레임)
    • public claim (공개 클레임)
    • private claim (비공개 클레임)
  • registered claim

    • 등록된 클레임들은 서비스에서 필요한 정보들이 아닌, 토큰에 대한 정보들을 담기 위하여 이름이 이미 정해진 클레임이다.
    • 등록 클레임은 모두 optional 이다.
    • iss : 토큰 발급자 (issuer)
    • sub : 토큰 제목 (subject)
    • aud : 토큰 대상자 (audience)
    • exp : 토큰의 만료시간 (expiration), 시간형식은 Numeric date (ex: 1480849147370) 이며, 항상 현재보다 이후로 설정되어있어야 한다.
    • nbf : 토큰의 활성 날짜와 비슷한개념, 시간형식은 Numeric date를 사용. 해당 날짜가 지나기 전까지는 토큰이 처리되지 않는다. (not before)
    • iat : 토큰이 발급된 시간 (issued at), 이 값을 사용하여 토큰의 age 가 얼마나 되었는지 판단 할 수 있다.
    • jti : JWT 고유 식별자, 중복처리를 방지하기 위해 사용된다. 일회용 토큰에 사용하면 유용하다. 예를들어서 client01이 jti가 id6098364921인 JWT를 발행하는 경우에는 client01에 의해 발행된 다른 JWT가 jti 값으로 id6098364921을 가질 수 없다. 다른 JWT와 동일한 jti 청구를 가진 JWT는 반복 공격으로 간주된다.
  • public claim

    • 공개클레임은 사용자 정의 클레임으로 공개용 정보전달을 위해 사용된다. 충돌을 방지하기 위해 클레임 이름을 URI 포맷으로 지정한다.

        {
            "https://kyle.com/jwt_claims/is_admin": true
        }
    • URI

      • 통합 자원 식별자 (Uniform Resource Identifier)
      • 인터넷에 있는 자원을 나타내는 유일한 주소. URI의 존재는 인터넷에서 요구되는 기본 조건으로서 인터넷 프로토콜에 항상 붙어 다닌다.
    • URL

      • 파일 식별자 (Uniform Resource Locator) / 유일자원지시기
      • 네트워크상에서 자원이 어디있는지 알려주기 위한 규약
      • URL은 웹사이트의 주소뿐만 아니라, 네트워크상의 자원을 모두 나타낼 수 있다.
    • 구분

      https://kyle.com/admin/post?type=board&category=1
      path 부분인 post까지가 URL
      protocol 부터 query string 까지 전부를 URI
      식별하는 부분 : ?type=board&category=1

  • private claim

    • 클라이언트와 서버 합의하에 사용되는 클레임이름들로, 등록/공개 클레임에 해당되지않는다.

      {
      				# --- 등록된 클레임
          "iss": "kyle.com",
          "exp": "1485270000000",
      				# --- 공개 클레임
          "https://kyle.com/jwt_claims/is_admin": true,
      				# --- 비공개 클레임
          "user_id": "1",
          "username": "kyle"
      			}
      import json
      import base64
      
      # 2개의 registered, 1개의 public, 2개의 private로 이루어진 claim
      payload = {
          "iss": "kyle.com",
          "exp": "1485270000000",
          "https://kyle.com/jwt_claims/is_admin": true,
          "user_id": "1",
          "username": "kyle"
      }
      
      pl = json.dumps(payload).encode("ascii")
      pl_encode = base64.b64encode(pl).decode("ascii")
      pl_encode.replace("=", "")
      >> 'eyJpc3MiOiAia3lsZS5jb20iLCAiZXhwIjogIjE0ODUyNzAwMDAwMDAiLCAiaHR0cHM6Ly9reWxlLmNvbS9qd3RfY2xhaW1zL2lzX2FkbWluIjogdHJ1ZSwgInVzZXJfaWQiOiAiMSIsICJ1c2VybmFtZSI6ICJreWxlIn0'

Signature

JWT의 마지막 부분으로 헤더의 인코딩값과 payload의 인코딩 값을 합친 후 주어진 비밀키로 해쉬하여 생성한다.

  • 서명 부분을 만드는 의사 코드(pseudo-code)의 구조

    HMACSHA256(
            base64_encode_haeder + "." + 
            base64_encode_payload,
            secret
    )
    import hmac
    import hashlib 
    import binascii
    
    secret = bytes('secret', "utf-8")
    msg = bytes(f"{pl_encode}.{hd_encode}", "utf-8")
    signature = hmac.new(secret, msg=msg, digestmod=hashlib.sha256).digest()
    base64.b64encode(signature).decode("ascii").replace("=", "")
    >> 'wRxdYTddvRsI6QuXCmYf9MVxj1sxpgxSNbszSADcbEg'

이제 만들어진 모든 값들을 . 로 합쳐주면 하나의 토큰이 완성된다.

eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJIQTI1NiJ9.eyJpc3MiOiAia3lsZS5jb20iLCAiZXhwIjogIjE0ODUyNzAwMDAwMDAiLCAiaHR0cHM6Ly9reWxlLmNvbS9qd3RfY2xhaW1zL2lzX2FkbWluIjogdHJ1ZSwgInVzZXJfaWQiOiAiMSIsICJ1c2VybmFtZSI6ICJreWxlIn0.wRxdYTddvRsI6QuXCmYf9MVxj1sxpgxSNbszSADcbEg

이 값을 https://jwt.io/ 의 debugger에 돌려보면 내용을 정상적으로 확인할 수 있다.

실제 서비스를 할때는 python에서 제공하는 jwt 라이브러리를 설정만해주면 손쉽게 만들고 검증까지 할 수있다.

JWT 라이브러리를 활용한 token 생성

import jwt
import pytz

from datetime import datetime, timedelta

def create_token(user_id):
    exp = datetime.now(pytz.timezone("UTC")) + timedelta(minutes=10)
		# 토큰의 만료시간 설정
    encoded_jwt_ = jwt.encode(
        {"user_id": user_id, "exp": exp.timestamp()},
        SECRET,
        algorithm="HS256",
    )
		# jwt 라이브러리를 사용하면 위의 과정을 함수를 호출하여 손쉽게 토큰생성을 할 수 있다.
    token = encoded_jwt_email.decode("UTF-8")

    return token, exp

JWT 라이브러리를 활용한 token 검증

import jwt

def check_user(access_token):
		try:
		    decode = jwt.decode(access_token, secret, algorithms=["HS256"])
		except jwt.exceptions.InvalidSignatureError:
		    return JsonResponse({"message": "TOKEN_INVALID"}, status=401)
		except jwt.ExpiredSignatureError:
		    return JsonResponse({"message": "TOKEN_EXPIRATION"}, status=401)
		except Exception as e:
		    return JsonResponse({"message": f"ERROR: {e}"}, status=500)
  • 이후에는 decode된 데이터를 토대로 유저의 유효성 검증을 진행하면된다
    • 예시
      • decode data의 비공개 클레임 내부에 존재하는 유저의 id를 가져와 유저가 존재하는지, 탈퇴한 유저는 아닌지 등을 검증

Why JWT

  • URL 파라미터와 헤더로 사용가능
  • 수평 스케일 용이 ( 서버의 대수를 늘려 서버의 처리속도나 데이터양을 개선시키는 것 )
    • 토큰자체에 정보를 포함하기 때문에 기존 방식에 비해 microservice 확장에 용이하다.
  • 디버깅 및 관리가 용이
  • 트래픽에 대한 부담이 낮음
  • REST 서비스로 제공가능 ( 클라이언트 - 서버 통신 )
  • 만료정보가 포함됨 ( 등록된 클레임 내부에 존재 )
  • 별도 저장소가 불필요하여 서버 자원을 절약할 수 있다.

JWT 저장방식

  • Session / Local
    • 특징 : statefull
    • 필수 데이터, 세션 / 사용자 id의 일부만 포함하고 나머지는 서버측에 저장하는 방식
    • 공격자가 토큰을 예측할 수 없고, 검색할 수 없다는 장점이 있어 CSRF(사이트간 요청위조) 공격으로부터 app을 보호할 수 있다.
    • 확장성에 제한이 있다
  • Cookie
    • 특징 : stateless
    • 브라우저의 쿠키에 저장하여 HttpOnly 플래그를 설정하여 XSS공격의 경우 토큰 도난으로부터 보호 할 수 있다.
    • 단점은 CSRF 보호를 기대할 수 없다는 것이다. (토큰은 쿠키와 자동으로 전송)

JWT 장점

무상태(stateless), 확장성(scalability)

  • 토큰은 client-side에 저장하기 때문에 stateless하다.
  • 사용자 인증에 필요한 모든 정보가 토큰 자체에 포함되기 때문에, 별도의 인증 저장소가 필요없다.
  • 마이크로 서비스 환경에서 중앙 집중식 인증 서버와 데이터베이스에 의존하지 않는 쉬운 인증 및 인가 방법을 제공한다.

확장성 (Extensiblity)

  • Scalability와 또 다른 개념으로 로그인 정보가 사용되는 분야를 확장하는 것을 의미한다.
  • 토큰을 사용하여 다른 서비스에서도 권한을 공유 할 수 있게된다.
    • github, google 계정으로 특정사이트에 로그인할 수 있는 것이 대표적인 예이다.

웹표준

  • JWT는 웹표준 RFC 7519에 등록되어있다.
    • 이는 여러 환경에서 지원되는것을 의미한다. (.NET, Java, Ruby, Python, PHP, Node.js ...)

JWT 단점

  • 토큰이 클라이언트에 저장되기떄문에 DB에서 사용자 정보를 조작하더라도 토큰에 직접 적용할 수 없다.
    • 사용자 계정을 차단하거나 비활성화 해야하는 경우 직접 토큰이 만료될 때 까지 기다려아한다.
  • 정보가 많아질수록 토큰이 커질 수 있다.
  • 비상태 app의 경우 토큰이 모든 요청에 대해 전송되므로 데이터 트래픽 크기에 영향을 미칠 수 있다.
  • 노출 가능성으로 인해 저장할 수 있는 정보가 제한적이다.
    • payload 자체는 암호화 된 것이 아니라 base64로 인코딩 된 것이기때문에, 중간에 payload를 탈취하여 데이터를 볼 수 있다.
    • 그렇기때문에 payload에는 중요한 정보를 넣지 말아야 한다.
    • 이러한 특징으로 JWT는 서명목적으로 사용된다.
  • 암호화가 풀릴 가능성을 배제할 수 없다.
    • 이 부분은 토큰의 만료기간을 짧게 설정하여 취약점을 최대한 보완할 수 있다.
profile
깔끔하게 코딩하고싶어요
post-custom-banner

0개의 댓글