JWT 토큰에 대한 이해와 보안의 중요성
JWT 토큰의 세 번째 부분: 서명(Signature)
- 서명은 JWT 토큰의 선택적인 부분입니다.
- 내부 애플리케이션 간의 신뢰할 수 있는 통신에서는 서명이 필요하지 않을 수 있습니다.
- 그러나 인터넷을 통해 클라이언트 애플리케이션과 JWT 토큰을 공유할 때는 서명이 필요합니다.
- 이는 네트워크 상에서 토큰의 헤더와 페이로드가 조작되지 않았음을 보장합니다.
서명의 역할과 생성
- 서명은 토큰이 생성된 후 데이터가 변경되지 않았음을 보장합니다.
- SHA-256 같은 알고리즘을 사용하여 서명을 생성합니다.
- 서명 생성 방법:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
- 여기서 secret은 백엔드 애플리케이션에만 알려져 있는 값입니다.
서명 검증 과정
- 서명 생성:
- 서버에서 JWT 토큰을 생성할 때, 헤더와 페이로드, 비밀 키를 이용해 서명을 생성합니다.
- 이 서명은 Base64로 인코딩된 값입니다.
- 서명 검증:
- 클라이언트가 요청을 보낼 때마다 서버는 헤더와 페이로드를 이용해 새로운 서명을 생성합니다.
- 생성된 서명을 기존 서명과 비교합니다.
- 두 서명이 일치하면, 토큰이 유효한 것으로 간주하고, 그렇지 않으면 조작된 것으로 간주합니다.
왜 JWT가 보안에 좋은지
- 변조 방지: 서명이 있는 JWT 토큰은 헤더나 페이로드가 조작되었을 때 이를 감지할 수 있습니다.
- 무결성 보장: 서명을 통해 토큰의 데이터가 변경되지 않았음을 보장합니다.
- 인증된 출처: 서명을 통해 토큰의 출처를 검증할 수 있습니다.
JWT 토큰 생성 과정
- Header와 Payload를 각각 Base64 URL 인코딩합니다.
- 인코딩된 Header와 Payload를 점(.)으로 결합합니다.
- 결합된 문자열과 비밀 키(secret)를 사용해 서명을 생성합니다.
- 최종적으로 header.payload.signature 형식의 JWT 토큰을 생성합니다.
1. Header:
{
"alg": "HS256",
"typ": "JWT"
}
이 JSON 객체를 Base64 URL 인코딩합니다:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
2. Payload:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
이 JSON 객체를 Base64 URL 인코딩합니다:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
3. Signature:
인코딩된 Header와 Payload를 점(.)으로 결합합니다:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
결합된 문자열과 비밀 키(secret)를 사용해 HMAC SHA-256 알고리즘으로 서명을 생성합니다:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
"your-256-bit-secret"
)
생성된 서명 예시
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
최종적으로 header.payload.signature 형식의 JWT 토큰을 생성합니다:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
JWT 토큰 검증 과정
클라이언트가 서버에 요청:
- 클라이언트는 JWT 토큰을 Authorization 헤더에 담아 서버에 요청을 보냅니다:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
서버에서 토큰 분리:
- 서버는 토큰을 header.payload.signature로 분리합니다:
header = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
payload = "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ"
signature = "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
서명 재생성 및 비교:
- 서버는 인코딩된 헤더와 페이로드를 사용해 새로운 서명을 생성합니다:
plaintext코드 복사new_signature = HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), "your-256-bit-secret" )
- 새로 생성된 서명 new_signature와 토큰에 포함된 서명 signature를 비교합니다.
- 서명이 일치하면 토큰이 유효하다고 판단하고 요청을 처리합니다. 서명이 일치하지 않으면 토큰이 조작되었다고 판단하고 요청을 거부합니다.
new_signature = HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
"your-256-bit-secret"
)
요약
- **비밀 키(secret)**는 서명을 생성하는 데 사용되며, 서버에서만 알고 있습니다.
- 서명은 비밀 키와 함께 헤더와 페이로드를 인코딩하여 생성됩니다.
- 클라이언트가 토큰을 조작하면, 서버에서 새로 생성한 서명과 토큰의 서명이 일치하지 않으므로, 서버는 이를 감지하고 유효하지 않은 토큰으로 처리합니다.