JWT를 구현하다보니 AI의 도움을 받아 구현을 어찌 저찌 했는데 도저히 동작원리를 한눈에 알기 어렵고 어떻게 토큰이 만들어져서 이게 인증에 쓰이는건지 궁금함을 참을 수 가 없었다. 그래서 하나하나 뜯어 알아보았고 내용이 많은 서버 개발자들에게도 유익할거라고 생각해 공유하기로 했다.
이 글에서는 다음 내용을 정리한다.
- JWT는 어떤 구조로 만들어지는가? (header.payload.signature)
- header는 누가 만들지? 랜덤 문자열인가?
- Base64 인코딩은 암호화인가?
- 서명(Signature)은 무엇이고, 왜 payload를 바꾸면 걸리는가?
.signWith(key, HS512)가 실제로 하는 일
1. JWT는 “암호화된 토큰”이 아니다
JWT는 흔히 “토큰”이라고 부르지만, 실제로는 세 덩어리 문자열로 구성된다.
header.payload.signature
중요한 포인트는 여기다.
JWT의 payload는 Base64로 인코딩되어 있을 뿐, 누구나 디코딩해서 내용을 볼 수 있다.
그래서 JWT payload에는 민감정보(이메일, 전화번호, 개인정보 등)를 넣으면 안 된다.
2. header는 누가 만들까? 랜덤인가?
JWT header는 개발자가 랜덤으로 만드는 게 아니다. 보통 JWT 라이브러리가 자동으로 만들어준다.
예를 들어 아래처럼 signWith를 지정하면 라이브러리는 내부적으로 header를 구성한다.
Jwts.builder()
.setSubject(userId.toString())
.signWith(key, SignatureAlgorithm.HS512)
.compact()
실제로 header는 이런 느낌이다.
{
"alg": "HS512",
"typ": "JWT"
}
alg: 어떤 알고리즘으로 서명했는지typ: 토큰 타입 (거의 항상 JWT)
즉, header는 랜덤이 아니라 “설정 정보(메타데이터)”에 가깝다.
3. Base64 인코딩은 “숨기기(암호화)”가 아니다
JWT에서 header/payload가 Base64로 인코딩되는 이유는 사람이 못 읽게 하려는 것이 아니다.
Base64는 전송/포맷 안정성을 위한 인코딩이다.
JWT는 HTTP Header, Cookie, URL 등에서 쓰이기 때문에 JSON 원문({ }, ", 공백 등)을 그대로 싣기 어려워 안전한 문자열 형태로 바꾸는 과정이 필요하다.
4. 그럼 “서명(signature)”은 뭐 하는 건데?
payload는 누구나 디코딩해서 볼 수 있다면… 공격자가 payload를 바꾸면 어떨까?
예를 들어 이런 payload가 있다고 하자.
{
"sub": "123"
}
공격자가 다음처럼 바꿔치기한다면?
{
"sub": "1"
}
서버가 이걸 그대로 믿으면 권한 탈취가 된다. 그래서 JWT는 “누가 만들었고 중간에 바뀌지 않았는지”를 증명해야 한다.
서명(signature)은 “토큰 내용이 서버가 만든 그대로임”을 증명하는 위조 방지 장치다.
5. jwt.secret을 Base64 decode하는 이유
Spring Boot 코드에서 자주 보는 이 라인:
private val key = Keys.hmacShaKeyFor(
Base64.getDecoder().decode(secretKey)
)
여기서 key는 signature 자체가 아니라, signature를 만들기 위한 비밀 도장(비밀 키)다.
jwt.secret: 보통 랜덤 바이트를 Base64로 인코딩해 설정 파일에 저장Base64 decode: 다시 원래 바이트 배열로 복원hmacShaKeyFor: HMAC 서명에 쓸 “키 객체”로 구성
즉, jwt.secret은 보안의 핵심이라 절대 외부에 노출되면 안 된다.
6. .signWith(key, HS512)가 실제로 하는 일
AccessToken 생성 메서드를 보자.
fun createAccessToken(userId: Long): String {
val now = Date()
return Jwts.builder()
.setSubject(userId.toString())
.setIssuedAt(now)
.setExpiration(Date(now.time + ACCESS_TOKEN_VALIDITY))
.signWith(key, SignatureAlgorithm.HS512)
.compact()
}
이 메서드는 내부적으로 다음 과정을 자동으로 수행한다.
6-1. header/payload 생성
- header: 라이브러리가 자동 구성 (
alg=HS512,typ=JWT) - payload: 우리가 넣은 값들 (
sub,iat,exp)
6-2. Base64URL 인코딩
encodedHeader = Base64UrlEncode(header)
encodedPayload = Base64UrlEncode(payload)
6-3. signature 생성 (HMAC-SHA512)
서명은 단순 문자열 결합이 아니다. HMAC이라는 암호학적 알고리즘으로 “위조 불가능한 지문”을 만든다.
dataToSign = encodedHeader + "." + encodedPayload
signatureBytes = HMAC_SHA512(key, dataToSign)
encodedSignature = Base64UrlEncode(signatureBytes)
6-4. 최종 토큰 완성
JWT = encodedHeader + "." + encodedPayload + "." + encodedSignature
즉,
.signWith(...)가 만들어내는 건 “encodedSignature”이며,.compact()가 최종 JWT 문자열을 완성한다.
7. 왜 header/payload가 signature에도 쓰이고, 토큰에도 다시 들어갈까?
처음 보면 “중복 아니야?” 싶은 구조다.
하지만 검증을 생각하면 필수다. 서버는 요청이 올 때마다 토큰에서 encodedHeader, encodedPayload를 꺼내고, 같은 key로 다시 HMAC을 계산해 signature가 일치하는지 확인한다.
expectedSignature = HMAC_SHA512(key, encodedHeader + "." + encodedPayload)
tokenSignature = encodedSignature
expectedSignature == tokenSignature ? OK : 위조
그래서 payload가 단 한 글자라도 바뀌면 서명이 완전히 달라지고, 서버는 위조를 즉시 감지한다.
8. 정리
- JWT는 암호화가 아니라 서명 기반이다
- payload는 Base64 인코딩이라 누구나 읽을 수 있다 → 민감정보 금지
- header는 랜덤이 아니라 라이브러리가 만드는 메타데이터
jwt.secret은 서명을 만들고 검증하는 비밀 키.signWith(key, HS512)가 encodedSignature를 생성한다- 최종 JWT는 encodedHeader.encodedPayload.encodedSignature
JWT는 “내용을 숨기는 장치”가 아니라, “내용이 바뀌지 않았음을 증명하는 장치”다.
다음 글에서는 HS512(HMAC)와 RS256(RSA)의 차이(대칭키 vs 비대칭키), 그리고 secret key 관리/rotate 전략까지 정리해볼 예정이다.
'개발' 카테고리의 다른 글
| 소셜 로그인을 위한 서버 설계(코드가 아닌 사전 설계) (0) | 2026.01.23 |
|---|---|
| GET 방식 API에서 Cache-Control을 중앙 통제로 관리하는 실무 전략 (0) | 2026.01.20 |
| 공간정보 뷰에서 데이터가 없으면 지점이 사라지는 문제와 SQL 튜닝 과정 — KEEP (DENSE_RANK LAST …) 활용하기 (0) | 2026.01.07 |
| HTTP 통신의 실제 모습 – 라이브러리가 감추고 있던 바이트 스트림 처리 (0) | 2025.12.09 |
| Spring Boot(Kotlin) — 12편. 완성된 프로젝트 템플릿 공유 (0) | 2025.11.27 |