인증(Authentication)과 인가(Authorization)
[Server] JWT(Json Web Token)란?
JWT (JSON Web Token) - 마이크로서비스를 위한 인증과 인가 - Opennaru, Inc.
jwt란 JSON 포맷을 이용하여 사용자에 대한 속성을 저장하는 claim 기반의 Web Token이다.
JWT의 특징은 가볍고, 토큰 자체에 정보가 포함되어있기 때문에, 정보로 사용하는자가 수용적인 방식으로 정보를 안전하게 전달한다.
클레임이란 사용자 정보나 데이터속성등을 의미한다.
클레임 기반 토큰은 토큰안에 정보를 담을 수 있는 특징이 있다.
클레임 기반 토큰은 아래와 같이 정보를 담고 있다. jwt는 클레임토큰중 가장 대표적인 것이다.
{
"id": "kyle1234",
"username": "kyle"
}
과거 일반토큰 기반은 의미가없는 문자열 기반으로 구성되어있다.
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 부분은 토큰에 담을 정보가 포함된다. 여기에 담는 정보의 한 '조각'을 클레임(Claim)이라고 하며, key
value
로 이루어져 있다.
클레임의 종류
registered claim
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
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'
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 라이브러리를 설정만해주면 손쉽게 만들고 검증까지 할 수있다.
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
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)