JWT ?
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. - Introduction to JSON Web Tokens (jwt.io) -
JWT (JSON Web Token) 은 토큰 자체에 json 객체 형태로 정보(클레임, claim)를 담고 있는 인증 표준. 데이터의 위/변조를 방지하기 위해 클레임을 HMAC, RSA Signature 등의 방식으로 서명 후 토큰에 포함시켜둔 것이 특징이다.
토큰의 구조
JWT 는 header, payload, verify signature 의 각 부분이 .
로 구분되어 있다.
- 예시 (jwt.io 에서도 확인가능)
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
- header :
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
- payload :
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
- verify signature :
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
- header :
Header
header 는 토큰 자체에 대한 정보를 담고 있다. 토큰이 어떤 형태인지 (typ
), 토큰이 어떤 알고리즘으로 서명되었는지 (alg
) 등을 담은 json 객체를 base64 로 인코딩하였다.
위 예시에서 헤더인 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
를 디코딩해 보자.
$ echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" | base64 --decode | jq .
{
"alg": "HS256",
"typ": "JWT"
}
위 토큰은 JWT 형식의 토큰이며, HS256 알고리즘을 사용하여 서명되었다. 일반적인 JWT 는 위 두 개의 프로퍼티를 가진다.
Payload
payload 에 담기는 인증 정보/권한 정보 등등을 claim 이라고 부른다. payload도 json 객체이며, claim은 payload 객체의 프로퍼티다.
예시의 payload eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
를 base64로 디코딩해 보자.
$ echo "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ" | base64 --decode | jq .
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
토큰에는 3개의 클레임이 담겨 있었다. 이 중 name
은 직관적으로 무슨 의미인지 파악이 가능하지만, sub
와 iat
는 의미를 바로 알기는 어렵다. 그 뜻을 파악하기에 앞서 클레임의 종류에 대해 먼저 살펴보자.
JWT claim은 registered, public, private 의 세 종류가 있다. registered 클레임은 RFC7519문서에서 미리 정의한 클레임이며, public 클레임은 RFC7519 문서에는 없지만 IANA JWT Claim Registry 에는 등록된 클레임이다. 클레임 레지스트리를 통해 JWT 문서에서는 정의하지 않는 클레임간의 이름과 의미 충돌을 방지한다고 한다. 이 외에 레지스트리에 등록하지 않고도 클레임을 자유롭게 정의하여 사용할 수 있는데, 이러한 클레임들을 모두 private 클레임이라고 부른다.
- registered claims (모든 클레임은 optional)
이름 | 의미 | 설명 |
---|---|---|
iss | Issuer | 토큰 발급자 |
sub | Subject | 토큰의 주제 |
aud | Audience | 토큰 수신자 |
exp | Expiration Time | 토큰 만료 시점. Unix Timestamp 형식의 숫자여야 함 |
nbf | Not Before | JWT 를 사용 가능한 시점. 토큰이 발급되었으나 이 시점 이전에는 사용이 불가능하다. Unix Timestamp 형식의 숫자여야 함 |
iat | Issued At | 토큰 발급 시점. Unix Timestamp 형식의 숫자여야 함 |
jti | JWT ID | JWT 의 Unique Identifier. |
다시 예시로 돌아가서 각 클레임들을 살펴보면 sub
는 Subject, iat
는 Issued At 이라는 의미의 registerd 클레임이며, name
은 OpenID Foundation 에서 Full name 의 의미로 등록한 public 클레임이다.
payload 에는 private claim 을 자유롭게 추가할 수 있지만 웬만하면 IANA JWT Claim Registry에서 private claim 의 이름이나 의미가 public claim 과 겹치는 것이 있는지 먼저 확인 후 사용해야 나중의 혼란을 예방할 수 있을 것이다.
Verify Signature
Verify Signature 는 토큰 데이터가 위조되는 것을 방지하는 기능을 한다. 토큰의 header 와 payload 부분은 암호화가 되어있지 않아 누구나 base64 로 디코딩&인코딩하여 원하는 데이터를 추가할 수 있는데, 데이터는 추가하더라도 서명 생성에 사용된 비밀 키를 모른다면 이 토큰을 사용할 수 없다. 서버에서 header 와 payload 의 서명을 만들어서 토큰의 서명과 비교하기 때문이다.
위 예시에서는 헤더의 alg
에 명시된 HS256
이라는 알고리즘으로 데이터가 서명되었음을 확인할 수 있다.
이 포스트에서는 “Verify Signature 덕분에 jwt 토큰의 내용이 위/변조로부터 안전하다” 정도의 설명만으로도 충분하니, 자세한 내용은 나중에 기회가 된다면 다루도록 하겠다.
장점
발견하는대로 계속 업데이트합니다
토큰에 정보를 담을 수 있어 DB 로드가 줄어든다.
토큰이 무의미한 랜덤 문자열인 경우 authentication, authorization 을 위해 데이터베이스에 쿼리를 보내야 한다. 성능 향상을 위해 Redis 등 InMemory DB 로 캐시해 두더라도 서버 밖으로의 네트워크 I/O 이기 때문에 성능 하락은 피할 수 없다.
하지만 JWT payload 에 사용자 정보와 권한 등의 내용을 담아두는 경우 서버에서는 signature 검증 후 바로 payload 데이터를 사용해 사용자 식별과 권한 확인이 가능하다. 네트워크 I/O 없이 사용자와 권한을 확인하는 것이니 서비스의 속도를 높일 수 있다.
작은 팁으로 JWT 에 어떤 데이터를 담을 지 구상할 때 DB 스키마와 함께 설계하는 것을 추천한다. JWT 에 payload 를 추가하여 DB 쿼리를 줄일 수 있는 이유는 JWT 가 DB 에 담긴 데이터를 포함하고 있어서인데, JWT payload 를 External Table (혹은 View) 로 생각하고 DB 스키마와 함께 설계한다면 어떤 데이터를 포함할 지 더욱 명확하게 확인할 수 있기 때문이다.
단점
발견하는대로 계속 업데이트합니다
길이가 길다
Payload 에 데이터가 들어 있어 토큰의 길이가 길어지므로 클라이언트와 서버 간 통신에서 요청 당 크기가 커진다. 따라서 트래픽을 1 Byte 라도 줄이는 것이 중요하다면 JWT 사용은 피하는게 좋다.
JWT는 보통 쿼리스트링이나 http 헤더에 사용하는데, 극단적인 경우 그 길이가 http 헤더 크기 제한을 넘어설 수도 있다.