포스트

JWT의 구조


이전 포스트는 기본적인 두 가지 보안 방식에 대한 개념과 토큰 기반 인증 방식인 JWT의 개념, 장단점에 대해 알아봤다.

이어서 이번에는 JWT의 구조에 대해 알아볼 것이다.

현재 진행 중인 ‘샐로그’ 프로젝트에서 멤버쉽 관련 담당은 나였다.

그렇기 떄문에 JWT에 대해 실사용해볼 기회가 많았고, 특히 리프레쉬 토큰으로 액세스 토큰을 재발급하는 과정 중에 JWT 디버거를 사용하여 토큰의 구조를 뜯어볼 기회가 많았다.

토큰의 구조를 분해하여 페이로드에 어떤 것이 담겨있는지 파악한 다음 리프레쉬 토큰을 활용해 액세스 토큰을 재발급하는 로직을 작성했다.

하지만 아직 근본적으로 이해가 부족한 느낌이 들어 JWT에 대해 다시 한 번 복습하는 만큼 이 구조 포스트에서는 토큰의 구조를 분해해보고 기본적으로 각 부위마다 어떤 내용이 포함되는지, 어떤 것을 의미하는지에 대해 알고있는대로 상세히 작성해볼 것이다.

특히 용어에 대해 작성하여 이후에는 헷갈릴 때 마다 이번 포스트를 보고 복습을 반복할 것이다.




1. Json Web Token의 구조

앞서 말했듯이 JWT는 토큰 기반 방식 중 가장 널리 이용되는 자격 증명 방식이다.

문자 그대로 문자열로 된 토큰을 활용하여 사용자의 자격을 판단하고 그에 맞는 역할 부여, 인증 과정을 거친다.

그렇다면 개발자로서 이 ‘문자열로 된 토큰’이 어떤 구조로 되어 있는지, 이 구조 내부에는 무엇이 담겨있는지를 파악해야 한다.

그래야 내가 의도한대로 사용자에게 인증용 토큰을 발급할 수 있을 것이고, 서비스의 보안에 대해 생각할 기회가 많아질 것이다.


우선 예시로 내가 실제 ‘샐로그’ 프로젝트에서 사용하는 인증 토큰을 기반으로 설명을 진행한다.

1
2
3
eyJhbGciOiJIUzUxMiJ9.
eyJyb2xlcyI6WyJVU0VSIl0sIm1lbWJlcklkIjoxLCJ1c2VybmFtZSI6InRlc3RNb2NrQGdtYWlsLmNvbSIsInN1YiI6InRlc3RNb2NrQGdtYWlsLmNvbSIsImlhdCI6MTcwNjA3MTczMiwiZXhwIjoxNzA2MDczNTMyfQ.
W4UDbRp9dLBkK8GW1psNj3gzwRn7O9FqxWHYsura0W2XgzzmPCJEwn0kUzW_waW1lAx3ePLg1picotgffZlcrA

이 문자열이 토큰이다.

문자열 타입으로 이루어져 있으며 마침표를 기준으로 각 문자열은 암호화 되어 있다.

이 암호화된 문자열을 복호화하면 해당 토큰의 내용이 드러난다.


우선 암호화된 문자열의 각 부분을 보면 아래와 같다.


예시로 가장 먼저 제시한 토큰은 복잡한 암호화를 위해 시크릿키를 최대한 길게 설정하였고, 담겨야 하는 자격 증명에 대한 정보가 많기 때문에 긴 문자열로 표현되어 있지만 기본적으로는 위 그림과 같은 구조이다.

토큰은 세 부분이 마침표로 구분된다.

각 부분은 헤더, 페이로드, 시그니쳐로 불린다.



1-1. JWT - 헤더

가장 먼저 포함되는 부분은 토큰의 ‘헤더’다.

이 헤더의 경우 복호화하면 다음과 같은 구조로 되어 있다.

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

위의 내용은 예시인데, 기본적으로는 이런 구조이다.

위 JSON 객체의 각 속성에 대해서는 다음과 같다.

  1. alg : 토큰이 생성될 때 사용된 해싱 알고리즘을 의미한다. 현재 값이 HS256인데, 이는 HMAC SHA-256을 사용하였음을 의미한다. 또한, 이 알고리즘은 토큰의 시그니처 부분을 생성하고 검증할때 사용된다.
  2. typ : 이 부분은 토큰의 타입을 나타낸다. 즉, 토큰의 종류를 말하는데, 위의 예시 객체에서는 JWT이기 때문에 타입이 JWT로 지정되어 있다. 이 정보를 바탕으로 서버가 토큰을 어떻게 처리해야 할지 결정할 수 있는 것이다.

다만, 필드 중 타입 속성은 선택사항이다.

보통 JWT를 처리하는 로직은 토큰의 구조와 시그니처를 보고 JWT임을 판단하기 때문이다.

‘샐로그’ 프로젝트에서도 마찬가지로 타입을 제외하고 헤더를 구성했다.

1
2
3
{
  "alg": "HS512"
}

이건 프로젝트에서 사용된 토큰을 복호화했을 때 포함된 헤더이다.

이 헤더에서는 위에서 말했듯이 타입에 대한 필드가 제외되었고, 알고리즘은 HMAC SHA-512가 사용되었다.



1-2. JWT - 페이로드

다음은 페이로드이다.

이 페이로드에는 서버에서 활용 가능한 사용자의 정보가 담겨있다.

어떤 정보에 접근 가능한지에 대한 권한 뿐 아니라, 사용자의 이름이나 식별자 등 서버에서 필요한 데이터를 담을 수 있다.

반대로 말하면, 이 페이로드에는 시그니쳐를 통해 유효성이 검증될 정보이지만 민감한 정보는 담지 않는게 좋다.

손쉽게 복호화가 가능하기 때문이다.

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

예시 토큰의 페이로드이다.

이 페이로드는 간단하게 되어 있다.

  1. sub : 서브젝트는 주제나 대상을 의미한다. 이 속성은 토큰이 어떤 것에 대한 것인지를 특정하고, 주로 고유 식별자를 사용한다.
  2. name : 이 필드는 이름을 의미한다. 이름 뿐 아니라 다른 것도 포함될 수 있다.
  3. iat : Issued At의 약자로 토큰이 발급된 시간을 의미한다.

이러한 페이로드에는 토큰에 대한 구체적인 정보인 클레임이 담기게 된다.

즉, 이 페이로드 부분을 바탕으로 사용자를 검증하게 된다.

1
2
3
4
5
6
7
8
9
10
{
  "roles": [
    "USER"
  ],
  "memberId": 1,
  "username": "testMock@gmail.com",
  "sub": "testMock@gmail.com",
  "iat": 1706071732,
  "exp": 1706073532
}

이 객체는 프로젝트에서 사용된 토큰에 담긴 페이로드이다.

가장 먼저 유저의 권한에 대해서, 그리고 이외에 필요한 것들이 담겨 있다.

  1. roles : 유저의 인가 처리를 위한 부분이다. 역할을 설정해 일반 유저인지, 어드민인지 등 우리 서비스에서 어디 까지 허용할 것인지를 나타낸다.
  2. memberId : 해당 유저의 식별자이다. 우리의 서비스를 이용하기 위해 헤더로 토큰을 보내 검증을 하게 되는데, 이 때 토큰을 바탕으로 복호화하여 회원 id를 가지고 로직에서 식별하기 때문에 포함되었다.
  3. username : 말 그대로 유저의 이름이다. 별개의 식별자를 두지 않고 사용자가 설정한 이메일 아이디를 그대로 식별자로서 사용한다.
  4. sub : 마찬가지로 고유 식별자로서 이메일 아이디를 활용했다.
  5. iat : 발급 시간이다.
  6. exp : 토큰 만료 시간에 대한 속성이다.

이 토큰의 페이로드를 바탕으로 서비스를 사용할 수 있도록 인증, 인가처리를 하게 된다.

그런데 복호화가 이렇게 간단히 되어 버리기 때문에 비밀번호, 결제 정보와 같은 민감한 정보는 포함하지 않는 것이 좋다.



1-3. JWT - 시그니처

시그니처는 JWT의 세 번째 파트로서 토큰의 무결성을 보장하기 위한 부분이다.

이 시그니처는 다음과 같은 순서로 헤더와 페이로드를 이용하여 생성된다.

  1. 헤더와 페이로드를 각각 Base64Url 인코딩한다.
  2. 인코딩된 헤더와 페이로드를 연결하고, 이 문자열에 비밀키를 사용하여 서명한다. 여기서 ‘서명’에 사용되는 알고리즘은 헤더에 명시된 alg 필드에 따라 결정된다.
  3. 이 서명을 다시 Base64Url 인코딩하여 시그니처를 생성하고 토큰에 포함한다.

이 순서를 거쳐 JWT 마지막 부분에 추가되고, 이를 통해 토큰이 전달 과정 중에 변조되지 않았음을 확인할 수 있다.

JWT를 수신한 서버는 동일한 방식으로 시그니처를 생성하고, 이를 토큰에 포함된 시그니처와 비교함으로써 토큰의 무결성을 검증할 수 있게 된다.

다음은 JWT의 시그니처를 생성하는 방법을 나타내는 코드이다.

1
HMACSHA256(base64UrlEncode(header) + '.' + base64UrlEncode(payload), secret);

이 코드는 내부부터, 헤더와 페이로드를 base64로 인코딩하고, 이 두 인코딩된 문자열을 마침표로 연결한다.

이 연결된 문자열에 대해 HMAC SHA-256 해싱 알고리즘을 적용한다.

이 때, 임의로 지정한 시크릿 키가 비밀 키로 사용된다.

이렇게 생성된 해시 값이 JWT의 시그니처가 된다.

이러한 방식으로 시그니처는 JWT의 마지막에 추가되고, 토큰의 무결성을 보장할 수 있게 된다.

이후 이 토큰을 받아들인 서버는 동일한 방식으로 시그니처를 생성하여 비교함으로써 무결성을 검증하게 된다.


하지만 주의해야할 점은 이 시그니처 자체는 ‘무결성’만을 보장하기 때문에 여전히 페이로드에 민감한 정보가 들어가면 안된다.

이 점을 주의하자.



이 과정들을 거쳐 JWT가 완성되고 토큰을 활용하여 회원의 인증, 인가 처리를 할 수 있게 된다.

그러나 계속해서 설명했듯이 토큰 자체를 복호화가 간단하기 때문에 최대한 그 내용, 특히 페이로드를 주의해야 한다.

다시 말해, 단순히 JWT 만으로 “안전하다.”라고는 할 수 없다는 것이다.

그래도 페이로드에 민감한 정보를 넣어 위험한 애플리케이션을 만들지는 말자.





이번 포스트는 JWT의 구조에 대해 좀 상세히 설명해보았다.

다음 번에는 ‘인증 서버’에 대한 글을 작성할 것이다.

사실 이 ‘인증 서버’는 뭘 의미하는지 아직 잘 모르겠다.

지금까지 JWT를 사용한 것은 단순히 이렇게 배웠으니까 정도였어서 이번 기회에 자세히 알아보자는 마음으로 블로깅을 하기 시작했다.

이 블로그는 저작권자의 CC BY 4.0 라이센스를 따릅니다.