목차
- 시작하며
- HTTP란?
- HTTPS란?
- 대칭키 방식 (OpenSSL)
- 공개키 암호화 방식
- 공개키와 연관된 인증서의 원리 (신뢰 제공)
- HTTPS를 이루는 3가지 기술
- SSL인증서의 기능
- CA(Certificate Authority)
- SSL 인증서 미리보기 (feat. 세종대학교의 인증서 CA는 무엇일까?)
- SSL인증서가 서비스를 보증하는 방법
- TLS/SSL 관점에서 통신의 3가지 절차
- SSL/TLS의 HandShake를 그림, 단계별로 이해하기
- JWT Token 구조
- Header
- VERIFY SIGNATURE
- JWT Token 기본적인 Library Interface 읽어보기
- JWT Token Interface 구현체 읽어보기
- 마무리
시작하며
안녕하세요! Passionfruit200입니다!
이번에 JWT를 적용하며 JWT의 각 라이브러리와 코드들을 읽어보았었는데, JWT Token의 서명 알고리즘인 HS256 대해 알아보며 자연스럽게 대칭키, 비대칭키, 그리고 이 원리가 적용된 SSL인증서 까지 자연스레 학습하게 되었습니다!
하지만, JWT에 대한 이야기를 먼저 두는것보다 HTTP, HTTPS, 대칭키, 비대칭키, 그리고 SSL 인증서를 먼저 설명해두는것이 흐름상 더 적절하다고 판단하여 순서를 변동하였습니다!
이번 글에서는 그 과정에서 배운것들을 정리해보았고, 추가로 글을 이해하기 위해서는 OSI 7 Layer의 각 프로토콜들에 대한 지식은 어느정도 필요합니다!
1-1. HTTP란?
HTTP는 HyperText Transfer Protocol의 줄임말입니다. HyperText는 문서와 문서가 링크로 연결되어있는 링크체계를 의미하는데, 단순하게 HTML이라고 생각하시면 될 것 같습니다.
즉, HTTP는 브라우저가 서버에 HTML 같은 웹문서를 달라고 요청하는 것 입니다.

1-2 HTTPS란?
그렇다면 HTTPS는 무엇일까요?
HTTP(S)에서 S는 HTTP + SSL/TLS 입니다.
즉, HTTP에 SSL/TLS 계층을 추가한 것입니다.(Over Secure Socket Layer)

여기서 앞에서는 HTTPS라고 해서 S인데 왜 TLS라고 부르는지 궁금합니다.
TLS는 SSL과 같은 의미이며, TLS로 바뀐 계기는 기존에 Netscape가 관리하던 SSL을 IETF로 관리조직이 바뀌면서 이름이 TLS로 변경되었다고 합니다.
HTTPS를 이해하기 위해 필요한 암호화 방식(대칭키 암호화 방식, 공개키 암호화 방식)
HTTPS의 핵심인 SSL/TLS를 이해하려면 먼저 “암호화”가 뭔지 알아야 합니다.
이를 위해 대칭키, 공개키에 대해 학습해보겠습니다!
대칭키 방식 (OpenSSL)
대칭키(Symmetric Key)는 암호화와 복호화할때 같은 키값으로 암/복호화 가능함을 의미합니다.
대칭키 방식은 말 그대로 같은 키로 암호화하고 같은 키로 복호화하는 방식입니다.
라는 구조입니다.
대칭키 방식 시나오

암호학의 영원한 단짝, Alice와 Bob을 예로 들어보겠습니다.
- Alice → 메시지를 암호화할 때 Passionfruit200 키 사용
- Bob → 받은 메시지를 복호화할 때도 Passionfruit200 키 사용
추가정보
Plaintext는 평문, .bin은 2진파일을 의미합니다.2진파일로 저장하는 이유는, 암호화된 데이터는 사람이 읽을 수 없는 수많은 바이트의 조합입니다. 텍스트로 저장하면 깨지거나 특정 문자 인코딩에서 문제가 생길 수 있으므로, 2진(Binary) 그대로 저장하는 것이 안전하고 표준적입니다.
대칭키 실습
[1] Alice가 Bob에게 “Hi, I am Alice” 평문 전송

PS C:\Users\user\ssl> echo 'Hi, I am Alice' > plaintext.txt
PS C:\Users\user\ssl> cat plaintext.txt
Hi, I am Alice
- Alice는 Bob에게 보낼 평문 plaintext.txt를 준비했습니다.
- 내용은 “Hi, I am Alice”
- 아직까지는 그냥 평문이라, 누가 보든 쉽게 읽을 수 있는 상태입니다.
[2] 네트워크로 보내기전에 Key(Passionfruit200)으로 암호화

PS C:\Users\user\ssl> openssl enc -e -des3 -salt -in plaintext.txt -out ciphertext.bin;
enter DES-EDE3-CBC encryption password:
Verifying - enter DES-EDE3-CBC encryption password:
*** WARNING : deprecated key derivation used.
Using -iter or -pbkdf2 would be better.
PS C:\Users\user\ssl> cat ciphertext.bin;
Salted__W훈?L?用_5v뿢t?唵?듮눫k~^??W雲也?
암호화가 끝나고 ciphertext.bin 내용을 열어보면 다음처럼 읽을 수 없는 값이 보입니다:
PS C:\Users\user\ssl> cat ciphertext.bin
Salted__W훈?L?用_5v뿢t?唵?듮눫k~^??W雲也?
[3,4] Bob이 받은 암호문을 같은 키(대칭키)로 복호화

PS C:\Users\user\ssl> openssl enc -d -des3 -in ciphertext.bin -out plaintext2.txt
enter DES-EDE3-CBC decryption password:
*** WARNING : deprecated key derivation used.
Using -iter or -pbkdf2 would be better.
PS C:\Users\user\ssl> cat plaintext2.txt
Hi, I am Alice
- Bob도 같은 키를 알고 있기 때문에 평문으로 완벽하게 복구할 수 있습니다.
여기까지 대칭키에 대해 알아봤습니다.
추가로, 대칭키의 장점은, 빠르고 효율적입니다. 그렇기 때문에 SSL에서도 실제 데이터 전송은 대부분 대칭키로 이루어집니다.
단점으로는, 키 전달이 위험합니다. 대칭키는 같은 키를 공유해야 하는데, 이 말은 Alice → Bob이 서로 통신을 할떄 서로 Key를 공유해야하는 사전작업이 먼저 필요하여 네트워크 상에서 키를 보내야하는데, 이떄 키를 누가 조작한다면 모든 메시지를 읽고 조작할 수 있습니다!
또한, 네트워크 구간이 길수록 키 노출 위험이 증가합니다. 인터넷망 → 통신사 → 라우터 → 여러 서버 → 목적지 서버 이 긴 경로를 지나가는 동안 중간 어딘가에서 sniffing(감청)하면 끝입니다. 이 때문에 “안전한 키 전달 방법”이 필요했습니다.
공개키 암호화 방식
이번에는 공개키 방식입니다. 공개키 방식은 키가 2개입니다.
1. 비공개키(Private Key)
2. 공개키(Public Key)
이 두 개의 키는 서로 “짝”입니다.
특징은, A키로 암호화한 건 반드시 B키로만 풀 수 있고, B키로 암호화한 건 반드시 A키로만 풀 수 있습니다.
사실, 공개키 방식이 나온 이유는 대칭키 방식의 단점을 보완하기 위해 나왔다고도 할 수 있습니다.키를 "전달하는 문제" 자체를 없앴기 때문입니다. 암호화하는 키와 복호화하는 키가 다르니 당연히 네트워크 상에서 1개의 키만 보내주면 됩니다.
공개키 시나리오

Alice → Bob에게 “Hi, I am Alice” 메세지를 보내려면:
- Alice는 Bob의 Private Key로부터 공개키(Public Key)를 가져옴
- Alice는 그 공개키로 "Hello Bob"을 암호화
- Bob은 자신의 개인키로만 복호화 가능
- 제3자는 공개키만 있어서는 복호화 불가능
위의 시나리오와 같습니다!
공개키 실습
이번에는 실제로 위 시나리오를 실습해보겠습니다.
1. 비공개키 생성
private key를 먼저 생성합니다.

PS C:\Users\user\ssl> openssl genrsa -out private.pem 1024;
PS C:\Users\user\ssl> cat private.pem
-----BEGIN PRIVATE KEY-----
MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKBJ9+Tm0mRM8xqt
f3i3Bj5AIiCkB3ofWp6ZyUxO4sobUTmiA6iC9CsxuKwIKqi3t9VIakjMaL01gRxi
dsoCMOqc4KmgjldebduhKB1+Nf+Qmhj7F0N5QgzDCFo4hhpP28OSMzWe3N3zuPEH
wmau2lAGU80UfH1oa5dNUssR0ksjAgMBAAECgYB/DQJ0Ks27eQ79J2ax3YkSUK18
Z+gRUcb3jfh0BtdW0b5ZS2VQ7bPyhO/XVIXTxAPwB/1PBM9EoqNbR2TXDFOOcmWb
8/3JOpRFGaywpbdGJZzvBXzbtnHqXpsuIF5wlPaLacZt3jjSCA2IWjyESuToFgQ5
hEqcaUQKqZBvww8huQJBAM7QIqLT0dTW/li9zHtB/Aobzr/ZqaNgXha4KiMS92a1
7+eQqYudzPwTp7kCpbJaxPcQCE/DFjvObOF4/u/nF+8CQQDGaTOp8n+zZts+L4NF
bwKM1x7qkDRp52AMMQ7vR2FIoQrQuZpCjX7osilXFUYDb52dQu7i6cORIA5vwh3D
lywNAkEAgeTD/FTh87Zc5cu/xKK69HZmsqS5IT4Dmm1tOb5N2RroZR68/k3MU37c
1xzMiWrtTueo8L/tFP8f77WZGYChzQJABKNY7dQZYBw7a8y4iNr7eEdfFaShVQhv
mllbPASzJXt+QTrVfFDKcq4XgU2iAVqOmKqD4xIL3EyficVD5NqX0QJATd56WTiT
f4hvqj/q2OeXOgGzTZesI5Bu60H3/2xbuLFoC8JYpCTKluPrT0L5M9VKZiI6qxnd
HrrjHAEYFMgrxA==
-----END PRIVATE KEY-----
2. 비공개키 → 공개키 만들기
private key를 기반으로 한 public key를 생성합니다. (이 private key를 해독할 수 있는 public key입니다.)

PS C:\Users\user\ssl> openssl rsa -in private.pem -out public.pem -outform PEM -pubout;
writing RSA key
PS C:\Users\user\ssl> cat public.pem
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCgSffk5tJkTPMarX94twY+QCIg
pAd6H1qemclMTuLKG1E5ogOogvQrMbisCCqot7fVSGpIzGi9NYEcYnbKAjDqnOCp
oI5XXm3boSgdfjX/kJoY+xdDeUIMwwhaOIYaT9vDkjM1ntzd87jxB8JmrtpQBlPN
FHx9aGuXTVLLEdJLIwIDAQAB
-----END PUBLIC KEY-----
3. 암호화할 평문 준비 & 공개키로 암호화하기

PS C:\Users\user\ssl> echo 'Hi, I am Alice' > plaintext.txt
PS C:\Users\user\ssl> cat plaintext.txt
Hi, I am Alice
PS C:\Users\user\ssl> openssl rsautl -encrypt -inkey public.pem -pubin -in plaintext.txt -out plaintext.ssl
The command rsautl was deprecated in version 3.0. Use 'pkeyutl' instead.
PS C:\Users\user\ssl> cat plaintext.ssl
sτ臺n8R!돫Gm뜒6??贇淵l??h좜팸쒥뾒??F)'?E慶n톊?mu凌}욇?뻟좥?(킎?養<D쮆-틴?c'뫴98|췗???e???lU뀑S
4. 비공개키로 복호화하기

PS C:\Users\user\ssl> openssl rsautl -decrypt -inkey private.pem -in plaintext.ssl -out plaintext2.txt
The command rsautl was deprecated in version 3.0. Use 'pkeyutl' instead.
PS C:\Users\user\ssl> cat plaintext2.txt
Hi, I am Alice
공개키와 연관된 인증서의 원리 (신뢰 제공)
만약 공개키로 암호화한다면 어떻게 될까요?
아래와 같이 2가지 경우로 될 수 있습니다.(앞에서 말했듯 공개키와 비공개키는 서로 간에 암호화/복호화하거나 복호화/암호화하는 관계입니다.)
- 암호화(Encrypt):
공개키로 암호화 → 비공개키로 복호화 - 인증(Signing):
비공개키로 암호화 → 공개키로 복호화
왜 비공개키로 암호화가 필요할까요?
“이 메시지가 진짜 내가 원하는 서버로부터 왔다는 것을 증명하고 싶기 때문"입니다.
누구나 공개키로 풀 수 있으니 비밀은 아니지만, “비공개키 소유자만 만들 수 있는 데이터”라는 사실이 인증된다는점이 SSL 인증서의 핵심 뼈대입니다.
HTTPS를 이루는 3가지 기술
HTTPS의 3가지 기술은 위에서 배웠었던
1. 대칭키 암호화
2. 공개키 암호화
3. 인증서 기반
이 세 가지를 섞어놓은 기술입니다.
공개키 암호화로 “대칭키를 안전하게 교환”하고,
인증서로 “서버가 진짜 서버인지 확인”하고,
그 뒤로는 “대칭키로 빠르게 통신”한다.
이 것이 HTTPS가 작동하는 방식입니다.
SSL인증서의 기능
인증서의 기능은 크게 두 가지입니다.
[인증서의 역할]
1. 클라이언트가 접속한 서버가 신뢰할 수 있는 서버임을 보장한다.
2. SSL 통신에 사용할 공개키를 클라이언트에게 제공한다.
이를 이해하기 위해 먼저 CA에 대해 먼저 알아보겠습니다.
CA(Certificate Authority)
위에서 말했듯이 인증서의 역할은 클라이언트가 접속한 서버가 클라이언트가 의도한 서버가 맞는지를 보장하는 역할을 합니다.
이 역할을 하는 민간기업들이 있는데 이런 기업들을 CA(Certificate authority) 혹은 Root Certificate라고 부릅니다.
현재 CA 점유율을 확인해보면, https://en.wikipedia.org/wiki/Certificate_authority 여기서 확인할 수 있습니다.

즉, SSL을 통해서 암호화된 통신을 제공하려는 서비스는 CA를 통해서 인증서를 구입해야 합니다.
하지만, 만약 개발이나 사적인 목적을 위해서 SSL의 암호화 기능을 이용하려 한다면 자신이 직접 CA의 역할을 할 수도 있습니다. 이것은 공인된 인증서가 아니기 때문에 브라우저는 안전하지 않음 이라는 메세지를 보여주는데 단순 개발목적이라면 상관없을 것 같습니다. (물론 사용자가 존재할경우에는 안됩니다.)
SSL 인증서 미리보기 (feat. 세종대학교의 인증서 CA는 무엇일까?)
먼저 SSL 인증서를 직접 확인해봤습니다. (로그인 페이지로 가면 반드시 확인할 수 있습니다.)

발급대상의 일반이름(CN)을 보면, 세종대학교(*.sejong.ac.kr) 와일드카드 도메인(서브 도메인을 한번에 보호하는 인증서)을 사용하는 것을 확인할 수 있었습니다.
즉, mail.sejong.ac.kr, www.sejong.ac.kr ,sw.sejong.ac.kr, cms.sejong.ac.kr, anything.sejong.ac.kr 를 한번에 보장할 수 있습니다.
두번째로는 CA정보를 확인했습니다.

이미지를 보면, ROOT -> Intermediate -> Domain 순으로 Parent -> child로 향하고 있습니다. (Tree 구조)
ROOT CA는 USERTrust RSA Certification Authority
Intermeditate CA는 Sectigo RSA Domain Validation Secure Server CA 인걸 확인할 수 있습니다!
SSL인증서가 서비스를 보증하는 방법

1. 웹브라우저가 서버에 접속할떄 서버는 제일 먼저 인증서를 제공한다.
2. 브라우저는 이 인증서를 발급한 CA가 자신이 내장한 CA의 리스트에 있는지를 확인한다.
3. 확인결과 서버를 통해서 다운받은 인증서가 내장된 CA 리스트에 포함되어 있다면 브라우저 안에 이미 저장되어 있는 CA의 공개키를 이용해서 인증서를 복호화한다. ( CA의 공개키를 이용해서 인증서를 복호화할 수 있다는 것은 이 인증서가 CA의 비공개키에 의해서 암호화된것을 의미한다. 그 정보의 출처를 신뢰할 수 있다. 공인된 기관이 이 사이트는 믿어도되! 라는 것이 암시되어있습니다. )
추가로 인증서안에는 그 서비스의 공개키가 포함되어있습니다. 이는 이후에 매우 중요한 역할을 하므로 기억하고 갑니다.
서버가 안전하다는 것을 인증서가 보증한 후에 공개키 암호화 방식으로 SSL 통신할경우
위 과정에서 서버가 안정하다는 것을 판단할 수 있었습니다.
이제 본격적으로 TLS/SSL에서 SSL 암호화가 어떻게 동작하는지 알아보겠습니다.
TLS/SSL 관점에서 통신의 3가지 절차
먼저, TLS/SSL 암호화 통신에서 전체 흐름에서 이 과정이 언제 일어나는지 큰 그림을 확인해보겠습니다.

우리가 학습할 부분은 TLS/SSL 관점에서 통신의 3가지 절차 중 TLS HandShake 부분입니다! (빨간 네모로 쳐진 부분)
SSL/TLS의 HandShake를 그림, 단계별로 이해하기
먼저, 아래의 그림을 보기전에 결론부터 말하겠습니다.
1. SSL은 암호화된 데이터를 전송하기 위해서 공개키와 대칭키를 혼합해서 사용합니다.
2. 즉 클라이언트와 서버가 주고 받는 실제 정보는 대칭키 방식으로 암호화하고,
3. 대칭키 방식으로 암호화된 실제 정보를 복호화할 때 사용할 대칭키는 공개키 방식으로 암호화해서 클라이언트와 서버가 주고 받습니다.
이해하기 어렵죠?
아래 그림으로 한번 이해해보시기 바랍니다..

위 그림의 빨간 번호가 아래 번호와 매칭되니 번호와 함께 보시기 바랍니다.
1. Client Hello 단계 – 클라이언트가 통신 조건 제시
이 단계에서 클라이언트는 서버에게 “아, 클라이언트가 이런 암호화 기법을 쓸 수 있구나”를 알려줍니다.
서로 통신하기전에 어떤 Protocol을 사용할지 정한다고 보면 됩니다.
Client Hello 메세지
- Client Random 전송
- 지원 가능한 Cipher Suites 목록 전송 & 지원 TLS 버전 전송
- Session ID 전송 (이전 세션 재사용 가능 여부를 위해)
2. Server Hello & 인증서 전송 – 서버가 조건 수락 + 인증서 제공
서버가 Client Hello에 응답하여, Server Hello Message & 인증서 전송합니다.
Server Hello 메시지
- Server Random 전송
- 서버가 선택한 Cipher Suite 안내
- 서버 인증서 전송 (공개키 + CA Signature 포함) & 이 인증서가 이후 모든 암호화의 시작점이 됩니다.
3. 인증서 검증 – 클라이언트가 서버의 신뢰성 확인
클라이언트는 서버의 인증서가 신뢰할 수 있는지 확인합니다.
- 브라우저/OS에 내장된 Root CA List와 비교
- 내장된 CA의 공개키로 CA Signature를 복호화
- 성공하면 인증서 안에 들어있는 서버 공개키를 신뢰하게 됩니다.
4. Pre-Master Secret 생성 & 서버 공개키로 암호화 후 전송
본격적으로 암호화 통신에 사용할 재료를 교환하는 단계입니다. 이때 pre Master Secret이라는 값을 클라이언트에서 생성해서 서버에 전송합니다. pre Master Secret이라는 값을 반드시 기억합니다.
- 클라이언트는 pre master secret(비밀 재료)을 랜덤으로 생성
- 하지만 이걸 평문으로 서버에 보내면 도청 가능하므로 서버 인증서 안의 공개키로 pre master secret을 암호화하여 서버로 전송 ( 서버는 자신의 비밀키로만 복호화할 수 있기 때문에 중간에서 탈취해도 무용지물됩니다.)
- 서버와 클라이언트는 둘만 공유하는 비밀값 pre master secret을 갖게 됩니다.
5. Master Secret 생성 – 서로 같은 세션 기반 키 생성
브라우저와 서버는 Master Secret 생성하는 작업을 동일하게 수행합니다.
- pre master secret + Client Random + Server Random 이용하여 동일한 Master Secret 생성합니다. ( 이 값은 외부에서는 절대로 계산 불가능합니다. )
6. Session Key 생성 – 실제 암호화 통신에 쓰이는 키
브라우저와 서버는 일련의 과정(이 부분은 TLS의 공식인 PRF 를 이용해 계산한다고 합니다.) 을 거쳐 Session Key 를 생성합니다.
- Master Secret을 이용해 대칭키(Session Key) 를 생성합니다.
- 앞서 공개키 방식은 계산량이 커서 실제 데이터 암호화에는 비효율적이므로, 이 이후부터는 대칭키 방식을 사용해 빠르게 암호화/복호화를 진행합니다.
7. 결론
즉, 앞으로 클라이언트와 서버 간 통신데이터는 모두 Session Key 로 암호화됩니다!
둘 다 같은 Session Key를 가지므로 대칭키 복호화 가능합니다.
상당히 복잡한 과정인데, 굳이 이렇게 대칭키 + 공개키 방식을 함께 혼합해서 사용하는 이유는 결국 "비용"입니다. 네트워크 통신 간에 매번 암호화 작업이 필요할텐데, 대칭키의 경우 Computing Power가 많이 소요되므로 공개키 방식을 사용하여 더 적은 비용으로 통신할 수 있습니다!
그리고 이어지는 JWT
시작하며에서 말했지만, JWT Token을 먼저 학습하고 자연스레 대칭키, 비대칭키로 넘어갔었는데 위의 과정을 먼저 이해하는게 더 정석인것같아 이제부터는 좀 뜬금없지만 JWT 내용이 나옵니다! JWT의 서명 알고리즘 부분에서 갑작스레 이런 궁금증이 시작되었으니, 한번 그 부분도 살펴보시기 바랍니다.
JWT Token 구조
토큰 기반 인증에서 가장 활발하게 사용되고 있는 JWT Token의 구조에 대해 알아보겠습니다. 위의 JWT Token 구조를 보면 알 수 있듯이, 크게 Header, PAYLOAD, VERIFY SIGNATURE 로 이루어져 있습니다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Header
- "alg" (Algorithm): 토큰의 서명 알고리즘을 지정합니다. JWT는 일반적으로 HMAC SHA-256 (HS256) 또는 RSA SHA-256 (RS256) 등의 알고리즘을 사용합니다.
- "typ" (Type): 토큰의 유형을 지정합니다. JWT의 경우 "JWT"로 설정됩니다.
이러한 헤더 정보는 JSON 객체로 표현되고 Base64로 인코딩됩니다.
{
"alg": "HS256",
"typ": "JWT"
}
PAYLOAD
JWT의 페이로드는 사용자 정의 데이터를 포함합니다. 이 데이터는 클레임(Claims)의 형태로 구성되며, 클레임은 토큰에 담길 정보를 의미합니다. 클레임은 다음과 같이 두 가지 유형으로 나뉩니다.
- 등록된 클레임(Registered Claims): 토큰의 표준 정보를 의미합니다. 예를 들어 "sub" (Subject), "iss" (Issuer), "exp" (Expiration Time), "iat" (Issued At), "nbf" (Not Before) 등이 있습니다.
- sub (Subject): "sub" 클레임은 토큰이 대상으로 하는 주체(Subject)를 나타냅니다. 주로 사용자를 식별하는 고유한 식별자가 이 부분에 들어갑니다. 예를 들어, 사용자 아이디나 이메일 주소 등이 될 수 있습니다.
- iss (Issuer): iss 클레임은 토큰을 발급한 발급자(Issuer)를 나타냅니다. 일반적으로 토큰을 생성한 서버의 도메인 또는 식별자가 이 부분에 들어갑니다.
- exp (Expiration Time): exp 클레임은 토큰의 만료 시간(Expiration Time)을 나타냅니다. 이 시간 이후에는 토큰이 더 이상 유효하지 않습니다. 시간은 일반적으로 UNIX 타임스탬프 형식으로 표현되며, 이를 초 단위로 나타낸 값이 이 클레임에 포함됩니다.
- iat (Issued At): iat 클레임은 토큰이 발급된 시간(Issued At)을 나타냅니다. 이 시간은 일반적으로 토큰이 생성된 시간을 나타내며, exp 클레임과 마찬가지로 UNIX 타임스탬프 형식으로 표현됩니다.
- nbf (Not Before): nbf 클레임은 토큰의 유효 시작 시간(Not Before)을 나타냅니다. 이 시간 이전에는 토큰이 유효하지 않습니다. "exp" 클레임과 "iat" 클레임과 마찬가지로 UNIX 타임스탬프 형식으로 표현됩니다.
- 비공개 클레임(Private Claims): 사용자 정의 정보를 포함할 수 있는 클레임으로, 서버와 클라이언트 간의 임의적인 정보를 주고받을 수 있습니다.
페이로드(Payload) 역시 JSON 객체로 표현되고 Base64로 인코딩됩니다.
{
"sub": "user123",
"name": "John Doe",
"admin": true
}
VERIFY SIGNATURE
JWT의 서명(SIGNATURE)은 Header와 Payload를 합친 후, 비밀 키로 해시를 생성하여 생성됩니다. SIGNATURE은 토큰이 변조되지 않았음을 검증하기 위해 사용됩니다. 서명은 헤더와 페이로드를 인코딩한 문자열과 비밀 키를 사용하여 HMAC SHA-256 또는 RSA SHA-256 알고리즘 등으로 생성됩니다.
예를 들어, 만약 헤더와 페이로드가 Base64로 인코딩된 후 다음과 같은 문자열이 된다고 가정하면
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
위의 문자열과 비밀 키(SECRET KEY)를 활용하여 서명(SIGNATURE)을 생성합니다. 서명은 토큰의 유효성을 검증하는 데 사용됩니다. 토큰이 변조되면, 서명이 일치하지 않게 되므로 토큰의 무결성을 보장할 수 있습니다.
위의 HEADER, PAYLOAD, VERIFY SIGNATURE 가 합쳐지게 되면 JSON Web Token이 됩니다.
JWT Token 기본적인 library Interface 읽어보기
기본적인 JWT 객체 클래스에 대해 알아봅니다.
Jwt와 Jws는 엄밀히 말하면 다르지만, 일반적으로 통용되는 이야기로 하여 JWT 객체에 대하여 알아보았습니다.
1. JWT.java (Interface)
public interface Jwt<H extends Header, B> {
H getHeader();
B getBody();
}
- JWT Interface입니다.
- 실제 Json Web Token으로 해당 JWT를 주고 받는다고 생각하면 됩니다.
- Header를 Extends 받은 H를 매개변수로 받고, B는 JWT의 Body를 나타냅니다. Payload 혹은 Claims 를 의미한다고 생각하면 되겠습니다.
- H getHeader();
- 이 메서드는 JWT의 헤더를 반환합니다.
- 반환 타입인 H는 Header 인터페이스 또는 이를 구현한 클래스입니다.
- 헤더가 존재하지 않을 경우 null을 반환합니다.
- B getBody()
- 이 메서드는 JWT의 페이로드를 반환합니다.
- 반환 타입인 B는 페이로드를 나타내는 타입으로, 보통 문자열(String) 또는 Claims 객체 등이 될 수 있습니다.
- 페이로드가 존재하지 않을 경우 null을 반환합니다.
- H getHeader();
- 이렇게 Jwt 인터페이스를 정의함으로써, JWT 토큰을 헤더와 Claims 로 나누어서 다룰 수 있게 됩니다.
- 여기서 의문인점은 Jwt에 Header, Body, Signature로 이루어져있는데 Signature는 어디있을까? 라는 의문이 생깁니다. 우리가 사용하는 JWT는 사실 JWS 입니다. JWT는 일종의 추상클래스 Abstract Class 라고 생각하면 되고, JWT에 우리가 Signature를 붙여서 사용하면 JWS라고 생각하면 됩니다.
- 이번 포스팅에서는 JWT 추상클래스에 대해 알아보고, JWS 내용은 생략합니다.
2. Header.java (Interface)
위의 JWT Interface에서 제너릭 타입 매개변수로 Header가 존재하여 확인해보았습니다.
위에서 말했듯 Jwt에는 Header, Payload, Signature로 존재합니다.
public interface Header<T extends Header<T>> extends Map<String,Object> {
public static final String JWT_TYPE = "JWT";
public static final String TYPE = "typ";
public static final String CONTENT_TYPE = "cty";
public static final String COMPRESSION_ALGORITHM = "zip";
@Deprecated
public static final String DEPRECATED_COMPRESSION_ALGORITHM = "calg";
String getType();
T setType(String typ);
String getContentType();
T setContentType(String cty);
String getCompressionAlgorithm();
T setCompressionAlgorithm(String calg);
}
- 우선 Header에서는 Map<String, Object>를 사용하기에 커스텀하기 위해 해당 클래스를 Extends하고 있습니다.
- JWT_TYPE, TYPE, CONTENT_TYPE, COMPRESSION_ALGORITHM 과 같은 Header에 들어가는 정보들이 들어가 있음을 확인할 수 있습니다.
3. Claims.java (Interface)
Registered Claims에 대한 정보를 확인할 수 있습니다.
public interface Claims extends Map<String, Object>, ClaimsMutator<Claims> {
public static final String ISSUER = "iss";
public static final String SUBJECT = "sub";
public static final String AUDIENCE = "aud";
public static final String EXPIRATION = "exp";
public static final String NOT_BEFORE = "nbf";
public static final String ISSUED_AT = "iat";
public static final String ID = "jti";
String getIssuer();
@Override //only for better/targeted JavaDoc
Claims setIssuer(String iss);
String getSubject();
@Override //only for better/targeted JavaDoc
Claims setSubject(String sub);
String getAudience();
@Override //only for better/targeted JavaDoc
Claims setAudience(String aud);
Date getExpiration();
@Override //only for better/targeted JavaDoc
Claims setExpiration(Date exp);
Date getNotBefore();
@Override //only for better/targeted JavaDoc
Claims setNotBefore(Date nbf);
Date getIssuedAt();
@Override //only for better/targeted JavaDoc
Claims setIssuedAt(Date iat);
String getId();
@Override //only for better/targeted JavaDoc
Claims setId(String jti);
<T> T get(String claimName, Class<T> requiredType);
}
- 공식적으로 제공하는 각종 Claims들이 있습니다.
4. ClaimsMutator.java (Interface)
위의 Claims interface 가 extends하고 있는 Interface입니다.
ClaimsMutator라는 이름처럼, ClaimsMutator 인터페이스에서는 JWT (JSON Web Token)의 Claims (클레임)에 대해 수정을 할 수 있는 함수들을 선언합니다.
ClaimsMutator를 extends하고 있는 Class가 있다면, 해당 Class에서 Claims (클레임)을 선언하거나 수정하는 Method를 구현한다는 의미입니다.
public interface ClaimsMutator<T extends ClaimsMutator> {
T setIssuer(String iss);
T setSubject(String sub);
T setAudience(String aud);
T setExpiration(Date exp);
T setNotBefore(Date nbf);
T setIssuedAt(Date iat);
T setId(String jti);
}
- Claims를 Setting 하는 Method들이 있습니다. (Header의 수정은 없습니다.)
- 여기서 <T extends ClaimsMutator> 라는 제네릭 타입 매개변수는 ClaimsMutator의 Extends하고 있는 Class만 매개변수로 들어오도록 하여 해당 Class Type으로 Return 값을 줍니다.
- 위에처럼 사용하는 이유 중 하나로는 빌더 패턴과 같은 상황에서 유용하게 사용할 수 있기 때문입니다. 이러한 제네릭 타입 매개변수를 활용하여 타입 안정성과 유연성을 확보할 수 있습니다.
5. JWTBuilder.java (Interface)
이제 JWT Interface를 확인했으니, Jwt를 만들어주기 위한 JWTBuilder Interface를 확인해보았습니다.
public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
JwtBuilder setHeader(Header header);
JwtBuilder setHeader(Map<String, Object> header);
JwtBuilder setHeaderParams(Map<String, Object> params);
JwtBuilder setHeaderParam(String name, Object value);
JwtBuilder setPayload(String payload);
JwtBuilder setClaims(Claims claims);
JwtBuilder setClaims(Map<String, Object> claims);
JwtBuilder addClaims(Map<String, Object> claims);
@Override //only for better/targeted JavaDoc
JwtBuilder setIssuer(String iss);
@Override //only for better/targeted JavaDoc
JwtBuilder setSubject(String sub);
@Override //only for better/targeted JavaDoc
JwtBuilder setAudience(String aud);
@Override //only for better/targeted JavaDoc
JwtBuilder setExpiration(Date exp);
@Override //only for better/targeted JavaDoc
JwtBuilder setNotBefore(Date nbf);
@Override //only for better/targeted JavaDoc
JwtBuilder setIssuedAt(Date iat);
@Override //only for better/targeted JavaDoc
JwtBuilder setId(String jti);
JwtBuilder claim(String name, Object value);
JwtBuilder signWith(SignatureAlgorithm alg, byte[] secretKey);
JwtBuilder signWith(SignatureAlgorithm alg, String base64EncodedSecretKey);
JwtBuilder signWith(SignatureAlgorithm alg, Key key);
JwtBuilder compressWith(CompressionCodec codec);
String compact();
}
- JwtBuilder를 만들기 위한 각종 함수들이 있습니다.
이외에도 Compression 관련 Interface와 Exception 등등 중요한 코드들이 존재하지만 생략하겠습니다.
이번 포스팅에서는 JWT의 Header와 Claims에 대한 Interface를 실제로 Implementation 한 코드를 확인해보겠습니다.
JWT Token Interface의 구현체들 읽어보기
1. DefaultJwt.class
JWT를 Implements 하고 있습니다.
public class DefaultJwt<B> implements Jwt<Header,B> {
private final Header header;
private final B body;
public DefaultJwt(Header header, B body) {
this.header = header;
this.body = body;
}
@Override
public Header getHeader() {
return header;
}
@Override
public B getBody() {
return body;
}
@Override
public String toString() {
return "header=" + header + ",body=" + body;
}
}
- 아까 Interface에서 확인한 Jwt<Header,B> 를 implements 하고 있습니다.
- Header와 Body를 매개변수로 받아서 DeafultJwt를 생성하고 있습니다.
2. DefaultHeader.class
Header Interface를 implements 하고 있습니다.
public class DefaultHeader<T extends Header<T>> extends JwtMap implements Header<T> {
public DefaultHeader() {
super();
}
public DefaultHeader(Map<String, Object> map) {
super(map);
}
@Override
public String getType() {
return getString(TYPE);
}
@Override
public T setType(String typ) {
setValue(TYPE, typ);
return (T)this;
}
@Override
public String getContentType() {
return getString(CONTENT_TYPE);
}
@Override
public T setContentType(String cty) {
setValue(CONTENT_TYPE, cty);
return (T)this;
}
@SuppressWarnings("deprecation")
@Override
public String getCompressionAlgorithm() {
String alg = getString(COMPRESSION_ALGORITHM);
if (!Strings.hasText(alg)) {
alg = getString(DEPRECATED_COMPRESSION_ALGORITHM);
}
return alg;
}
@Override
public T setCompressionAlgorithm(String compressionAlgorithm) {
setValue(COMPRESSION_ALGORITHM, compressionAlgorithm);
return (T) this;
}
}
- 코드를 보면 JwtMap을 Extends하고 있는데, DeafultHeader.class에서 Map<String, Object> 형식으로 Header를 다루는 방식을 확장하고 싶다면 할 수 있게 넣어둔 것 같습니다. 여기서는 따로 확장하여 사용하고 있지 않고, 아래의 DefaultClaims에서는 JwtMap을 확장하여 사용하고 있는 모습을 확인할 수 있습니다.
3. DefaultClaims.class
Claims를 구현하고, JwtMap을 확장하고 있습니다.
public class DefaultClaims extends JwtMap implements Claims {
public DefaultClaims() {
super();
}
public DefaultClaims(Map<String, Object> map) {
super(map);
}
//...생략
@Override
public String getIssuer() {
return getString(ISSUER);
}
@Override
public Claims setIssuer(String iss) {
setValue(ISSUER, iss);
return this;
}
//...생략
@Override
public <T> T get(String claimName, Class<T> requiredType) {
Object value = get(claimName);
if (value == null) { return null; }
if (Claims.EXPIRATION.equals(claimName) ||
Claims.ISSUED_AT.equals(claimName) ||
Claims.NOT_BEFORE.equals(claimName)
) {
value = getDate(claimName);
}
return castClaimValue(value, requiredType);
}
private <T> T castClaimValue(Object value, Class<T> requiredType) {
if (requiredType == Date.class && value instanceof Long) {
value = new Date((Long)value);
}
if (value instanceof Integer) {
int intValue = (Integer) value;
if (requiredType == Long.class) {
value = (long) intValue;
} else if (requiredType == Short.class && Short.MIN_VALUE <= intValue && intValue <= Short.MAX_VALUE) {
value = (short) intValue;
} else if (requiredType == Byte.class && Byte.MIN_VALUE <= intValue && intValue <= Byte.MAX_VALUE) {
value = (byte) intValue;
}
}
if (!requiredType.isInstance(value)) {
throw new RequiredTypeException("Expected value to be of type: " + requiredType + ", but was " + value.getClass());
}
return requiredType.cast(value);
}
}
- 여기서는 JwtMap의 get을 Extends하는 모습이 보입니다.
- T get 코드를 읽어보면,
- 클레임 이름(claimName)에 해당하는 값을 반환하는 것입니다. 클레임은 문자열, 숫자, 날짜 등 다양한 데이터 타입으로 저장될 수 있으므로, requiredType에 해당하는 타입으로 값을 변환하여 반환해야 합니다.
- 예를 들어, JWT 클레임의 값이 숫자로 저장되어 있을 때, requiredType이 Long으로 지정된 경우 해당 값을 Long으로 변환하여 반환합니다.
- castClaimValue 메서드의 목적
- 주어진 값을 requiredType에 해당하는 타입으로 변환합니다.
- 코드를 보면, JWT 클레임의 값이 Long으로 저장되어 있는데, requiredType이 Date로 지정된 경우 해당 값을 Date로 변환하여 반환합니다. 또한, 값이 Integer 타입인 경우에는 requiredType에 맞게 Long, Short, Byte 등의 숫자 타입으로 변환하여 반환합니다.
- 이러한 메서드들을 사용하여 DefaultClaims 클래스는 Claims 인터페이스의 메서드를 구현하고, 필요한 타입 변환과 데이터 유효성 검사로 JWT 클레임을 Parsing 하려고 하고 있습니다. 이 과정을 통해 JWT의 클레임 값을 원하는 자료형을 사용하여 얻을 수 있습니다.
마무리
이번 글에서는 자연스레 대칭키, 비대칭키, SSL 인증서, Jwt Class와 Jwt 구성요소인 Header, Claims 의 객체 구성에 대해 알아보았습니다.
Jwt의 Header, Claim 내용들을 직접 확인해보면서, Jwt의 구성요소에 대해 조금 더 잘알게 된 것 같습니다.
하지만, Jwt의 내용을 Parsing하는 과정을 진행하는 DeafultJwtParser와 실제로 Jwt의 서명(Signature)의 무결성을 검증하는 Jws에 대하여서는 다루지 않고 있습니다. 서명 관련해서는 코드를 보는것보다는 대칭키, 비대칭키, SSL 인증서를 학습하는게 더 좋을 것 같습니다.
해당 내용에 관심있는 분들은 DefaultJwtParser.class 와 DefaultJws.class 를 확인해보시면 좋을 것 같습니다.
또한 추가로 Compression 관련 코드들도 존재하니, 해당 내용들도 확인이 가능합니다.