유저를 인증하고 식별하기 위한 토큰(Token) 기반 인증
인증과 정보 전달을 위해 사용하는 데이터 묶음. 사용자의 권한 정보나 서비스를 사용하기 위한 정보가 포함된다
header, payload, signature로 구성된다.
키(kid), 타입(typ), 서명 암호화 알고리즘(alg)의 정보가 담겨있다.
예시 :
{
"alg": "RS256",
"typ": "JWT",
"kid": "12345"
}
alg : 토큰을 서명할 때 사용된 알고리즘. 여기서는 RS256이 사용됐다.
typ : 토큰의 타입. JWT는 항상 JWT로 설정된다.
kid : key ID.
여기서 키(kid)는 선택적으로 사용할 수 있다.
JWT가 위조되지 않았음을 보증하기 위해, JWT에 고유한 "도장"을 찍는 과정
encodedHeader + "." + encodedPayload
//예시
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjM0NTYifQ
이 데이터를 특정 키로 암호화
서버는 비밀키를 이용하여 암호화하거나 해시값을 생성하는데, 이 때 생성된 암호화값이나 해시값이 signature(서명)이다.
JWT에 서명을 붙임
encodedHeader + "." + encodedPayload + "." + signature
//예시
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjM0NTYifQ.abcdef123456
//최종
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjM0NTYifQ.abcdef123456
➡️ 서명은 비공개키로 생성되며, 공개키로 검증된다. 클라이언트가 서명을 확인하면, JWT가 변조되지 않았고 신뢰할 수 있다는 걸 알 수 있다.
JWT는 비대칭암호화 방식을 사용한다.
비대칭 암호화 : 공개키(누구나 볼 수 있는 키)와 비공개키(소유자만 아는 비밀키)를 사용하며, 비공개키로 암호화하여 공개키로 복호화하거나, 공개키로 암호화하여 비공개키로 복호화하는 방식이다.
JWT에서의 사용
JWT 생성 : 서버는 비공개키로 JWT를 서명한다.
JWT 검증 : 클라이언트나 다른 서버는 공개키를 사용해 JWT 서명을 검증한다. 이 때, 공개키만으로는 서명을 위조하거나 비공개키를 알아낼 수 없다.
이 때, 사용하는 키가 하나뿐이라면 kid는 굳이 쓸 필요 없다.
서버에 키가 여러개 있는 경우.
키 롤링(Key Rotation)을 하는 경우
보안을 강화하기 위해 오래된 키를 폐기하고 새로운 키를 사용하는 경우.
6개월마다 새로운 키를 생성한다면, 새 JWT는 새로운 키로 서명되고, 기존 JWT는 이전 키로 검증해야 하는데, 이 경우 kid가 있어야 어떤 키로 서명했는 지 알 수 있다.
다른 클라이언트별 키 관리
만약 여러 애플리케이션이나 클라이언트(예: 모바일 앱, 웹 앱)가 서버와 통신한다면,
각 클라이언트마다 고유한 키를 부여해서 관리할 수 있어 JWT의 kid를 통해 어떤 클라이언트가 보낸 JWT인지 식별 가능하다.
JWT를 발급하는 서버가 여러 개일 때
부하 분산이나 마이크로서비스 아키텍처에서 JWT 발급 서버가 여러 개라면, 각 서버마다 고유한 키를 사용할 수 있다. 검증할 때는 kid를 통해 어떤 서버의 키를 사용해야 하는지 알 수 있다.
부하분산: 트래픽을 여러 서버에 고르게 분배해 시스템이 과부하 없이 안정적으로 작동하도록 하는 기술.
마이크로서비스 아키텍처: 하나의 큰 애플리케이션을 여러 개의 독립적인 작은 서비스로 나눠 각각 관리하고 개발하는 방식. (유지보수와 확장성에 유리함)
서버가 공개키를 여러개 제공하는 경우
오래된 공개키와 새로운 공개키가 모두 JWKS(JSON Web Key Set)에 등록돼 있을 때,
kid를 통해 어떤 공개키를 사용해야 할지 선택하게 된다.
JWKS (JSON Web Key Set): JSON 형식으로 공개 키(public key) 정보를 담은 문서.
주로 JWT의 서명을 검증할 때 사용되며, 인증 서버에서 제공하는 URL로 가져올 수 있음.
Access Token과 Refresh Token으로 나누어 사용하는 경우
Access Token: API 요청에 바로 사용되는 단기 토큰.
Refresh Token: Access Token을 갱신하는 장기 토큰.
역할:
API 요청 시 인증을 증명하는 데 사용됨.
서버는 Access Token을 보고 사용자가 요청할 권한이 있는지 확인.
특징:
주로 짧은 만료시간(예: 5분~1시간)을 가짐.
유출되면 즉시 API를 호출할 수 있으므로 짧게 유지하는 게 중요.
클라이언트가 보호된 리소스(API)에 접근할 때 함께 전송
역할:
Access Token이 만료되었을 때, 새로운 Access Token을 발급받는 데 사용됨.
특징:
더 긴 만료시간(예: 7일~30일)을 가짐.
서버에서만 검증하며, API 요청에 직접 사용되지 않음.
유출 시 피해가 클 수 있으므로 반드시 안전한 저장소(예: HttpOnly 쿠키)에 저장.
보안 강화:
Access Token이 유출돼도, 만료되면 더 이상 사용할 수 없음.
Refresh Token은 서버에서만 검증되므로, 노출 위험을 줄임.
유연성:
Access Token의 만료시간을 짧게 설정해서 보안을 강화.
Refresh Token으로 사용자가 번거롭게 재로그인하지 않아도 되게 함.
API 요청 효율화:
Refresh Token은 오직 Access Token 갱신에만 사용되므로, API 요청에는 전혀 영향을 주지 않음.
토큰에서 사용할 정보의 조각들인 클레임(claim)이 담겨있다.
key:value 형태를 가진다.
저장되는 정보에 따라 등록된 클레임(Reserved Claims), 공개 클레임(Public Claims), 비공개 클레임(Private Claims)로 구분된다.
{
"iss": "myapp.com",
"sub": "1234567890",
"aud": "myapi",
"exp": 1712345678,
"iat": 1712345670,
"role": "admin",
"email": "user@example.com"
}
iss (issuer): 토큰 발급자.
sub (subject): 토큰의 주제, 주로 사용자 ID.
aud (audience): 토큰의 대상(누가 사용할지).
exp (expiration): 토큰 만료 시간(유닉스 타임스탬프).
iat (issued at): 토큰 발급 시간.
nbf (not before): 이 시간 이후부터 유효한 토큰.
{
"userId": "12345",
"role": "admin",
"email": "user@example.com"
}
header + payload와 서버에 있는 key값을 header에서 정의한 알고리즘으로 암호화한것
이를 통해 데이터의 무결성과 신뢰성을 보장한다.
header와 payload는 제3자가 복호화 및 조작할 수 있지만 signature는 서버의 비밀키가 유출되지않는 이상 복호화 할 수 없다.
데이터 무결성 보장:
Header와 Payload가 변경되지 않았음을 확인.
만약 누군가 Header나 Payload를 변경하면 서명이 유효하지 않게 된다.
발급자 신뢰 보장:
서명은 비공개키 또는 비밀키로 생성되기 때문에, 해당 키를 가진 서버만 JWT를 생성할 수 있어, 이를 통해 JWT를 신뢰할 수 있다.
1. Header와 Payload를 조합
Base64URL로 인코딩된 Header와 Payload를 .로 연결한다.
encodedHeader + "." + encodedPayload
2. 서명 알고리즘 적용
3. 서명 결과는 JWT의 세 번째 부분이 된다.
<encodedHeader>.<encodedPayload>.<signature>
확장성이 중요할 때: JWT는 서버가 상태를 관리하지 않아도(stateless) 되기 때문에, 서버 간 부하 분산이 쉽다.
예: 클러스터 환경에서 각 서버가 인증 데이터를 공유할 필요 없이 JWT로 인증 처리.
간단한 인증이 필요한 경우: 인증 과정이 단순하고, API 호출이 많은 서비스에 적합.
예: 소규모 서비스, SPA(Single Page Application) 등.
권한 부여가 필요한 경우: JWT의 Payload에 권한(scope) 정보를 담아서 특정 리소스에 대한 접근을 제어.
예: API 호출 시 사용자의 역할(Role)에 따라 다르게 처리.
유출되면 바로 위험: JWT는 소유자 중심(Bearer) 인증 방식이므로, 누군가 토큰을 탈취하면 해당 사용자처럼 행동할 수 있음.
서버에서 토큰 취소가 어렵다: JWT는 기본적으로 상태를 저장하지 않으므로, 유효기간이 남아 있는 동안은 무효화하기 어려움.
해결책: 짧은 만료시간 + Refresh Token 사용.
➡️ 금융 서비스나 초고보안 환경에서는 JWT만으로 충분하지 않을 수 있으니, 다른 방식(mTLS, SAML 등)과 병행해야 할 수도 있다.
HTTPS 사용: JWT는 Payload가 암호화되지 않으니, HTTPS로 전송해 탈취를 방지.
짧은 만료시간 설정 (exp): Access Token의 만료시간을 짧게 설정하고, Refresh Token으로 연장.
민감한 정보는 절대 Payload에 포함하지 않기: 비밀번호, 카드 정보 같은 민감 데이터를 넣지 말고, 서버에서 필요할 때만 조회.
비대칭키 사용 (RS256): 서버 간 신뢰 관계가 필요하다면, 비대칭키로 서명해 보안을 높임.
Token Blacklist 관리: 토큰이 유출되거나 강제 로그아웃 시 사용 못 하게 서버에서 블랙리스트 관리.
Refresh Token의 탈취에 대비해서, Refresh Token은 서버에 저장되어 관리되어야한다. 이렇게 되면 완전한 stateless는 아니게된다. 하지만 Refresh Token을 저장해도, JWT 기반 인증이 주는 확장성과 장점을 유지하면서 보안성을 강화할 수 있다.
Access Token은 여전히 Stateless:
Access Token은 클라이언트에서 직접 관리하고, 서버는 이를 검증하기만 한다.
Access Token이 만료되기 전까지는 별도의 상태 관리 없이 인증이 가능.
보안 강화를 위해 필요:
Refresh Token을 서버에 저장하면 탈취되더라도 즉시 무효화할 수 있다. 무한히 Access Token을 발급받는 문제를 효과적으로 차단할 수 있다.
확장성: Access Token 검증은 서버 상태를 참조하지 않아도 가능하기때문에, 분산 환경에서 Access Token은 여전히 유효하게 작동.
클라이언트 중심 인증: Access Token은 클라이언트에 저장되며, 요청마다 서버에 인증 상태를 조회하지 않아도 된다.
주요 인증 로직은 여전히 Stateless: Refresh Token 관리는 Access Token 만료 시에만 작동하므로, 주요 인증 흐름은 여전히 Stateless.
일반적인 웹/모바일 서비스: 보안 수준이 기본적인 SSL/TLS로 충분히 보장된다면 굳이 Refresh Token을 자주 갱신할 필요가 없다.서버 부하와 관리 복잡성만 증가한다.
서비스 확장성이 중요한 경우: 상태 저장이 많아지면, 확장성과 분산 처리가 어려워질 수 있다.
보안이 매우 중요한 서비스:
예: 금융 서비스, 의료 서비스.
Refresh Token 유출에 민감한 환경에서는 토큰을 자주 갱신하여 위험을 줄일 수 있다.
유출 가능성이 높거나 의심되는 환경:
예: 공용 네트워크나 디바이스에서 사용하는 서비스.
➡️ 민감한 정보를 다룬다면, 함께 발급해서 보안 강화를 추천하지만 일반적인 애플리케이션에서는 Refresh Token시 재인증을 시키는 방식으로도 충분하다.
참고 :
JWT (Json Web Token)의 구조와 사용하기
Access Token과 Refresh Token이란 무엇이고 왜 필요할까?