본문으로 건너뛰기

JWT (Json Web Token)

JWT(Json Web Token) 은 통신 정보를 JSON 형식을 사용하여 안전하게 전송하기 위해 사용됩니다. JWT는 토큰 자체에 정보가 포함되어 있는 Claim 기반 토큰입니다. 일반적인 애플리케이션에서 JWT는 주로 인증과 인가를 구현하기 위해 사용됩니다. JWT헤더, 페이로드, 시그니처로 구분됩니다. 헤더에는 토큰의 암호화 알고리즘이나 타입을 가지며, 페이로드에는 데이터(만료일, 사용자 정보 등)를 가집니다. 시그니처헤더페이로드변조되지 않았는지 판단하기 위해 사용되는데요. 헤더페이로드를 비밀 키를 사용하여 헤더에 명시된 암호화 알고리즘으로 암호화하여 시그니처가 만들어집니다.

✔️ JWT 구조

JWT는 Header, Payload, Signature의 3 부분으로 이루어지며, JSON 형태인 각 부분은 Base64로 인코딩 되어 표현됩니다. 또한 각각의 부분을 이어 주기 위해 . 구분자를 사용하여 구분합니다.

JWT Structure

1) 헤더 (Header)

토큰의 헤더는 두가지 정보(typ, alg)로 구성됩니다. alg는 헤더(Header)를 암호화 하는 것이 아니고, 서명(Signature)을 해싱하기 위한 알고리즘을 지정합니다.

alg

해싱 알고리즘을 지정, 보통 HS256, RSA가 사용되며, 서명(Signature) 및 토큰 검증에 사용합니다.

typ

토큰의 타입을 지정합니다.

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

2) 페이로드 (Payload)

페이로드에는 토큰에서 사용할 정보의 조각들인 클레임(Claim)이 담겨 있습니다. 여기에 담는 정보의 한 ‘조각’ 을 클레임(claim) 이라고 부르고, 클레임(claim)은 key-value의 한 쌍으로 이루어져 있습니다. 클레임(claim)은 총 3가지로 나누어지며, 토큰에는 JSON(`key-value) 형태로 여러 개의 클레임(claim)을 넣을 수 있습니다.

2-1) 등록된 클레임 (Registered Claim)

등록된 클레임들은 서비스에서 필요한 정보들이 아닌, 토큰에 대한 정보들을 담기위하여 이름이 이미 정해진 클레임들입니다. 등록된 클레임의 사용은 모두 선택적 (optional)이며, 이에 포함된 클레임 이름들은 다음과 같습니다:

Claim이름설명
issIssuer토큰 발급자(issuer)
subSubject토큰 제목(subject)
audAudience토큰 대상자(audience)
expExpiration토큰 만료 시간(expiration), NumericDate 형식으로 되어 있어야 함 ex) 1480849147370
nbfNot Before토큰 활성 날짜(not before), 이 날이 지나기 전의 토큰은 활성화되지 않음
iatIssued At토큰 발급 시간(issued at), 토큰 발급 이후의 경과 시간을 알 수 있음
jtiJWT ID토큰 고유 식별자(JWT ID), 중복 방지를 위해 사용하며, 일회용 토큰(Access Token) 등에 사용

2-2) 공개 클레임 (Public Claim)

공개 클레임은 사용자 정의 클레임으로, 공개용 정보를 위해 사용됩니다. 공개 클레임들은 충돌이 방지된 (collision-resistant) 이름을 가지고 있어야 합니다.

❗️Public Claim 충돌 시의 문제점

문제 유형설명
🔄 의미 불일치서로 다른 시스템이 동일한 클레임 이름을 다른 의미로 해석할 수 있음
🔒 보안 리스크공격자가 의도적으로 충돌되는 클레임을 삽입하여 권한 우회를 시도할 수 있음
🐞 디버깅 어려움클레임 충돌로 인한 문제가 발생해도, 문제를 추적하고 파악하기가 어려움

🧨 충돌 예시

시스템"role" 클레임 해석
A (내부 서비스)서로 다른 시스템이 동일한 클레임 이름을 다른 의미로 해석할 수 있음
B (외부 인증 서버)공격자가 의도적으로 충돌되는 클레임을 삽입하여 권한 우회를 시도할 수 있음

👉🏻 이럴 경우, 의미는 다르지만 이름과 값이 일치해서 혼동 발생
👉🏻 내부 시스템은 외부에서 온 "admin"을 최고 권한으로 오해하고 권한 과다 부여

충돌을 방지하기 위해서는,

  • 클레임 이름을 URI 형식으로 짓거나,
{
"https://johnycho.com/claims/department": "finance",
"https://johnycho.com/claims/role": "admin"
}
  • IANA에 공식 등록된 public claim 이름만 사용합니다.
    👉🏻 위의 충돌 예시의 방지 대책으로 공식 클레임인 roles(principal(자신이 누구라고 주장하는 주체)이 보유한 role(역할) 목록)을 사용

2-3) 비공개 클레임 (Private Claim)

비공개 클레임은 사용자 정의 클레임으로, 서버와 클라이언트 사이에 임의로 지정한 정보를 저장합니다. 외부에 공개되도 상관없지만 해당 유저를 특정할 수 있는 정보들을 담습니다.

{
"username": "johny"
}

3) 서명 (Signature)

서명(Signature)은 토큰을 인코딩하거나 유효성 검증을 할 때 사용하는 고유한 암호화 코드이다. 서명(Signature)은 위에서 만든 헤더(Header)와 페이로드(Payload)의 값을 각각 BASE64로 인코딩하고, 인코딩한 값을 비밀 키를 이용해 헤더(Header)에서 정의한 알고리즘(alg)으로 해싱을 하고, 이 값을 다시 BASE64로 인코딩하여 생성한다.

3-1) 서명 (Signature) 생성 과정

  1. 헤더(JSON)를 Base64Url 인코딩
{
"alg": "HS256",
"typ": "JWT"
}

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

  1. 페이로드(JSON)를 Base64Url 인코딩
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ

  1. 인코딩된 헤더와 인코딩된 페이로드를 .(점)으로 이어붙임.
<Base64Url(Header)> + "." + <Base64Url(Payload)>

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ

  1. 위 문자열을 비밀 키와 함께 지정된 알고리즘(HMAC-SHA256 등)으로 해싱
HMACSHA256(
data = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ",
key = "your-256-bit-secret"
)

49f94ac7044948c78a285d904f87f0a4c7897f7e8f3a4eb2255fda750b2cc397

👉🏻 SHA-256은 항상 256비트 길이의 해시 값을 출력
👉🏻 HMAC-SHA256 결과 = 256비트 = 32(256 ÷ 8)바이트 = 16진수 64글자(16진수 1글자: 2바이트 x 32바이트)

  1. 나온 해시 값을 Base64Url로 다시 인코딩 → 이게 서명(Signature)

SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

✔️ JWT 전달 방법

JWT는 일반적으로 HTTP 요청의 인증 정보(Access Token)로 사용되며, 다음과 같은 방식으로 서버에 전달됩니다.

1) Authorization 헤더 사용

GET /api/user
Host: example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

👉🏻 Bearer는 토큰 유형을 나타냄 (기본 스펙)
👉🏻 OAuth2, OpenID Connect에서도 이 방식을 따릅니다

2) 쿠키에 포함

Set-Cookie: access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c; HttpOnly; Secure; SameSite=Strict

👉🏻 클라이언트가 다음 요청 시 자동으로 쿠키를 보냄
👉🏻 서버는 Cookie 헤더에서 JWT 추출

⚠️ 단점

브라우저 보안 정책상 다른 도메인(origin)으로 요청할 때, 쿠키는 기본적으로 포함되지 않기 때문에 CSRF 대응이 필요합니다. (→ SameSite + CSRF Token 같이 써야 함)

3) 쿼리 스트링 방식

GET /api/user?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

❗️URL이 로그에 남거나 공유될 수 있고, 링크 클릭 시 Referer 헤더를 통해 외부로 노출될 위험이 있기 때문에 일반적으로 권장되지 않습니다.

✔️ JWT 장점 / 주의 사항

JWT를 사용하여 인가를 구현하는 경우, 클레임 기반 토큰의 특성 덕분에 세션 기반 인증에 비해서 사용자 정보를 조회하기 위한 추가적인 작업이 필요하지 않습니다. 또한, 서버가 상태를 관리하지 않기 때문에 서버가 이중화된 환경에서도 사용자의 로그인 정보를 일관성 있게 관리할 수 있습니다. (세션 불일치 문제가 발생하지 않습니다.)

하지만 JWT를 사용하는 경우, 몇 가지 주의 사항이 존재합니다.

  • JWT는 디코딩이 쉽습니다. Base64로 디코딩하면 페이로드를 확인할 수 있습니다. 따라서, 민감한 정보를 담는 것에 유의해야 합니다.
  • 시크릿 키의 복잡도가 낮은 경우, 무작위 대입 공격(Brute force Attack)에 노출될 수 있습니다. 따라서, 강력한 시크릿 키를 사용하는 것이 권장됩니다.
  • 시크릿 키는 유출되면 안되기 때문에 안전한 공간에 관리해야합니다.
  • JWT 탈취에 유의해야 합니다. 이를 위해서 JWT 저장 공간, 리프레시 토큰 도입 여부, Refresh Token Rotation, 탈취 감지 및 대응에 대해서 고민이 필요합니다.
  • 토큰의 잦은 갱신이 사용자 경험을 저해하는지 고려해야 합니다. 예를 들어, 사용자가 게시글을 3시간 동안 작성하고 제출했지만 JWT가 만료되어 사용자가 작성한 글은 사라질 수 있습니다. 이를 해결하기 위해서 슬라이딩 세션과 같은 전략을 고민해 볼 수 있습니다.
  • JWT none 알고리즘 공격을 유의해야 합니다. 공격자가 토큰의 헤더에 명시된 알고리즘을 none으로 변경하여, 페이로드가 변조되어도 시그니처 검증을 우회할 수 있습니다. 이를 해결하기 위해서 none 알고리즘 공격을 예방한 라이브러리(ex) jjwt(io.jsonwebtoken))를 사용하거나, none 알고리즘과 같이 약한 알고리즘에 대해서 필터링하는 등 주의가 필요합니다.
Loading comments...