JWT 가 뭔데?

웹의 기능제공자(서버)와 클라이언트간 데이터를 주고받기 위한 수단으로
주로 사용자를 서버에서 인증하기 위해 사용한다.

JWT는 헤더, 페이로드, 서명으로 구성된다.
헤더와 페이로드는 JSON 문자열(key - value)을 base64 방식으로 인코딩 한 데이터이고,
이를 검증하기 위한 서명부분으로 구성되어 있다.

헤더(Header)

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

헤더는 일반적으로 두 가지 정보를 포함하고 있다.

  • alg : 해싱 알고리즘 종류
    • HMAC SHA256 : 키 1개 필요
    • RSA : 공개키 + 비밀키 총 2개 필요
  • typ : JWT

페이로드(Payload)

1
2
3
4
5
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}

페이로드는 클레임을 포함한 JWT 의 두번째 부분이다.
위 예시에서 "sub": "1234567890" 같이 하나의 프로퍼티를 클레임이라고 하며,
일반적으로는 엔터티 및 추가데이터에 대한 실질적인 데이터를 포함하고 있다.
모든 클레임은 서버와 그 사용자가 정의하기 나름이지만, 미리 정의된 클레임도 존재한다.

클레임에는 아래와 같이 크게 3 가지 유형으로 나눌 수 있다.

  1. Registered claims : 필수는 아니지만, JWT 사용자 간 원활한 통신을 위해 미리 정의해 둔 클레임이다.
    RFC7519 4.1 section 명세서에 명시되어 있다.
  2. Public claims : JWT 를 사용하는 당사자가 정의할 수 있는 사용자 정의 클레임이다.
    다만, 충돌을 피하기 위해 공개 클레임은 IANA JSON Web Token Registry에 정의해야 한다.
  3. Private claims : 사용자간 정보를 공유하기 위해 만들어진 사용자 정의 클레임

여기서 조금 헷갈릴 수도 있는 부분이 Registered claims vs Public claims이다.

Registered claims 는 RFC Specification 에 정의된 규칙이다.
예를 들면, iss : Issuer, sub : Subject, aud : Audience, exp : Expiration Time
으로 정의되어 있는데, 각 의미의 클레임을 JWT 페이로드에 사용하려면 반드시 저런 key 값으로 정의되어야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public interface Claims extends Map<String, Object>, ClaimsMutator<Claims> {

/** JWT {@code Issuer} claims parameter name: <code>"iss"</code> */
public static final String ISSUER = "iss";

/** JWT {@code Subject} claims parameter name: <code>"sub"</code> */
public static final String SUBJECT = "sub";

/** JWT {@code Audience} claims parameter name: <code>"aud"</code> */
public static final String AUDIENCE = "aud";

/** JWT {@code Expiration} claims parameter name: <code>"exp"</code> */
public static final String EXPIRATION = "exp";

/** JWT {@code Not Before} claims parameter name: <code>"nbf"</code> */
public static final String NOT_BEFORE = "nbf";

/** JWT {@code Issued At} claims parameter name: <code>"iat"</code> */
public static final String ISSUED_AT = "iat";

/** JWT {@code JWT ID} claims parameter name: <code>"jti"</code> */
public static final String ID = "jti";

...
}

JWT의 Java 라이브러리를 뜯어보면, Claims 라는 인터페이스 내부에 Registered Claims 7가지가 미리 정의되어 있는 것을 볼 수 있다.

반면, Public Claims 는 라이브러리 내에서 확인할 수 있는 맵은 없다. 다만, email, uuid 등과 같이
잘 알려진 프로퍼티에 대해서 미리 claims 명을 정의해놓은 것이 바로 public claims 이고, 이를 IANA에서 확인 해봐야 한다.
IANA 에서 미리 확인하고 충돌되지 않도록 설계하라는 것이다.

스타크래프트를 예로 들어보면, 게임사에서 정의한 승리 목표는
‘상대방의 건물을 모두 파괴하라’ 이고, 이런 승리를 elimination 이라고 한다.
상대방의 건물을 모두 파괴하면, 유닛과 자원이 얼마가 남아있건 간에 승리하게 된다.

반면 비공식적으로 게임의 흐름상 자신의 패배가 확실하다거나, 혹은 더 이상 게임을 유지할 수 없을 정도로 멘탈이 박살나면,
채팅으로 GG(Good Game)을 치고, 상대방의 승리를 인정하며 게임에서 나가는 문화가 있다.

elimination 이 Registered Claims (공식적인 규칙) 이라면,
gg는 Public Claims 이다.

아주 간략하게 정리해보면 이렇게 구분할 수 있다.

  1. Registered Claims : 진짜 미리 정의된 클레임
  2. Public Claims : 아마도 미리 정의된 클레임
    • 일반적인 jwt 사용 수준에서 ‘이거 정의되어 있을라나?’ 싶을만한 것들은 거진 다 정의되어 있다고 보면 된다.
  3. Private Claims : 나머지 수준에서 jwt 발급자와 사용자가 협의한 클레임

서명(Signature)

서명의 사전적 정의는 ‘본인의 자필로 이름을 써넣는 것’으로 공적 신뢰도를 보장하는 상징적 수단이다.
가령 서명이란 서명한 자 A가 아닌 다른 제 3자 누군가가 보더라도 A가 서명했다라는 것을 인정할 수 있을만한 공적 수단이란 것이다.

이런 의미에서 JWT의 서명 부분은 JWT로 표현된 데이터가 서명한 자에 의해 발급되었다는 것을 증명할 수 있는 수단이다.

방법적으로는 헤더와 페이로드를 각각 base64로 인코딩한 후,
각 해싱 알고리즘(SHA256 or RSA)을 통해 각 알고리즘에 맞는 키로 해싱을 거쳐 완성된다.

1
2
3
4
5
6
7
8
9
10
11
-- SHA256의 경우
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload), secret-key
)

-- RSA의 경우
RSASHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
public-key,
private-key
)

(+, -, /, =) 같은 URL에는 포함될 수 없는 문자(URL-safe)가 기본적인 base64 인코딩에는 포함될 수 있다.
따라서, base64 인코딩 시 URL-safe 하도록 인코딩하는 것이 base64UrlEncoding 이다.

JWT 왜 씀?

JWT는 실세계의 놀이동산 티켓과 매우 유사한 점이 많다.

놀이공원에서 놀이기구를 타는데까지 프로세스를 정리해보자.

  1. 입구 매표소에서 티켓을 구매한다.
    • 만약 예약한 사람이라면, 신분증과 함께 예약한 티켓을 받는다.(인증)
    • 티켓에는 유효기간(당일)이 있다.
    • 티켓의 종류에 따라 입장권,자유이용권,빅5 등으로 나눈다.
  2. 구매한 티켓으로 놀이기구에 줄을선다.
  3. 놀이기구 운용직원은 티켓을 확인하고 각 권한에 맞도록 이용하게 해준다.(인가)
    • 입장권 : 이용 불가 –> 이용권 추가구매
    • 자유이용권 : 입장을 허용하며, 대기줄로 안내한다
    • 빅5 : 만약 빅5 놀이기구라면 바로 이용할 수 있도록 안내한다.

JWT 도 위 메커니즘과 정확히 일치한다.

  1. 자신의 id / pwd 로 로그인 하여, jwt 를 서버로부터 발급받는다.
    • jwt 에는 유효기간(iat)이 존재하므로, 특정 시간 후에는 사용할 수 없다.
  2. 발급받은 jwt 를 통해 특정 기능에 대한 인증과 인가를 동시에 받을 수 있다.

JWT 어떻게 씀?

java의 경우 io.jsonwebtoken:jjwt를 사용한다.
해당 라이브러리에 있는 Jwts.builder를 사용하여 header, payload, signature를 쉽게 지정 가능하다.

1
2
3
4
5
6
7
8
public String createToken() {
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(validity)
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}

생각해보자

JWT 는 웹 어플리케이션이 Resource 위주의 MSA(Micro Service Architecture)로 발전하게 되면서,
효율적인 보안을 위해 탄생한 기술이다.

항상 보안이란 단어가 나오면

  • 사용자의 편의성
  • 보안적인 안전성
  • 성능 차원에서의 효율

자신의 시스템이 3가지 중 어떤 것을 중요하게 보는 가치인지 선택해야하는 순간이 오게된다.
JWT 는 효율적인 보안기술이란 점에 있어서 많이 사용되고 있긴 하지만, 여전히 탈취나 검증부분에서 취약점이 분명하게 존재한다.
따라서, 해킹방법이나 사용자의 편의성 차원에서 refresh token, sliding session 등의 기술을 도입하려 한다면,
안전성 차원에서 반드시 검토가 필요하다.

만약 SHA256 방식의 해싱 알고리즘을 사용한다면, Secret key는 정말정말 안전하게 보관해야 한다.
또한, JWT 를 제대로 사용하려면, JWT 만 알아선 안된다는 생각이 들었다.

JWT 의 취약점을 면밀히 추가학습해야하고, 해킹방법, 해싱 알고리즘에 대한 이해가 어느정도 필요하다는 생각이 들었다.
따라서 다음 섹션엔 추가 학습에 대한 키워드를 남기고 추가 학습해 볼 예정이다.

추가 학습 키워드

  • 보안 관점에서 JWT 의 취약점
  • Sliding Session, Refresh Token
  • Secret key 관리
  • 암호화 알고리즘 종류
  • Session - Cookie

참고자료