JWT(JSON Web Token)는 오늘날 웹 애플리케이션과 API에서 사용자 인증 및 정보 교환을 위해 널리 사용되는 기술이다. 실제로 실무에서도 많이 쓰이고, 면접 질문에서도 단골 질문으로 나오는 이 JWT에 대해서 알고 있다고 생각했지만, 어느날 신입 개발자가 뭐냐고 물어봤을 때 확신에 차서 JWT는 뭐다! 라고 대답하지 못하는 자신을 발견했다.
이 블로그 포스팅을 통해 JWT에 대해 다시 한번 정확히 알아보며, JWT가 왜 탄생하게 되었는지 그 등장 배경부터 시작해서, 어떻게 동작하는지, 어떤 장점과 단점을 가지고 있는지 알아보자.
JWT 이전의 인증 방식
고전적으로, 웹 애플리케이션의 주된 인증 방식은 세션-쿠키 기반 인증이었다. 이 방식의 동작 원리는 다음과 같다.
- 사용자가 로그인을 시도.
- 서버는 정보가 유효한지 확인하고, 유효하다면 사용자의 정보를 서버 메모리나 데이터베이스(이하 '세션 저장소')에 저장한다. 이때, 각 사용자를 식별할 수 있는 고유한 ID(세션 ID)를 생성한다.
- 서버는 이 세션 ID를 클라이언트에게 보내고, 클라이언트는 이 ID를 쿠키에 저장한다.
- 이후 클라이언트는 서버에 요청을 보낼 때마다 쿠키에 저장된 세션 ID를 함께 보낸다.
- 서버는 받은 세션 ID를 세션 저장소에 있는 정보와 비교하여 사용자를 식별하고 인증 상태를 확인한다.
이 방식은 구현히 간단하고, 직관적이지만 서버는 세션 저장소를 만들고 관리해야하며, 사용자가 많아질 수록 세션 저장소 부하가 증가하고, 그에 따라 병목 현상등으로 이어져 서버가 감당하기 힘들어질 수 있다.
또한 값을 쿠키에 담아서 보내기 때문에 CORS 정책으로 인한 크로스 도메인 이슈가 있으며, 웹 브라우저가 아닌 모바일 앱이나 다른 종류의 클라이언트에서는 쿠키를 직접 관리하기 번거롭거나 적합하지 않을 수 있어, 이는 클라이언트 다양성을 고려할 때 제약 사항이 되었다.
웹의 진화와 새로운 인증 방식의 필요성
2000년대 중반부터 웹은 단순한 정보성 웹사이트를 넘어, 웹 서비스 시대로 진입했다. 이러한 변화가 기존 인증 방식에 새로운 문제를 가져 왔다.
- 제3자 서비스 통합: 사용자들이 페이스북, 구글 등의 계정으로 여러 서비스에 로그인하기를 원함.
- API 인증: 단순히 웹사이트 접속을 넘어, API를 통해 데이터를 주고받는 경우에도 인증이 필요해짐.
- 크로스 도메인 문제: 쿠키는 기본적으로 동일한 도메인에서만 유효하기 때문에, 여러 도메인에 걸쳐 서비스를 제공하거나 여러 도메인 간의 인증이 필요한 경우에 문제 발생.
이러한 문제들을 해결하기 위해 오늘날 널리 사용되는 OAuth가 등장했다. OAuth는 여러 차례의 발전을 거쳐 OAuth 2.0으로 발전했으며, 이는 토큰을 통해 사용자의 권한을 확인하는 방식을 채택했다.
OAuth 2.0이 인증 플로우(흐름)를 정의했지만, 실제로 사용되는 토큰의 형식은 명확히 정의하지 않았다는 것이다. 이로 인해 각 서비스는 자체적인 토큰 형식을 사용하게 되었고, 이는 또 다른 형태의 비효율성을 야기했다.
SAML
API 기반 인증이 대중화되기 전, 기업 환경에서는 이미 Single Sign-On(SSO) 의 필요성이 있었다. SAML은 2002년 OASIS에서 개발한 XML 기반의 인증 및 권한 부여 표준으로. 주로 기업 환경에서 SSO를 구현하는 데 사용되었다.
하지만 SAML은 XML형식으로 되어있는 만큼 파일이 크고, 파싱과 서명 검증이 복잡했고. REST 방식에는 적합하지 않았다.
JWT(JSON Web Token) 탄생
앞선 배경을 바탕으로 JWT는 2010년대 초반, IETF(Internet Engineering Task Force)의 OAuth 워킹 그룹에서 개발되었다. JWT는 두 개체 간에 안전하게 클레임(claims)을 전송하기 위한 컴팩트하고 독립적인 방법을 정의한 개방형 표준(RFC 7519)이다.
JWT가 등장하면서 표준화된 토큰이 나왔고, JSON 기반으로 XML(SAML)보다 데이터 크기가 작아 HTML 및 HTTP 환경에서 효율적으로 전달이 가능해졌다. 또한 JWT는 자체적으로 필요한 정보를 담고 있어, 서버가 별도의 세션 정보를 저장하지 않아도 된다.
앞선 배경을 바탕으로 JWT는 2010년대 초반, IETF(Internet Engineering Task Force)의 OAuth 워킹 그룹에서 개발되었다. JWT는 두 개체 간에 안전하게 클레임(claims)을 전송하기 위한 컴팩트하고 독립적인 방법을 정의한 개방형 표준(RFC 7519)이다.
JWT의 등장으로 아래와 같은 문제점들을 해결했다.
- 표준화된 토큰: 토큰 형식이 표준화되어, 서비스 간의 상호 운용성이 크게 향상되었다.
- 효율적인 데이터 전송: JSON 기반으로 만들어져 기존 XML(SAML) 방식보다 데이터 크기가 훨씬 작다. 따라서 HTML 및 HTTP 환경에서 더욱 효율적으로 데이터를 주고받을 수 있게 되었다.
- 서버 무상태성(Stateless): JWT는 토큰 자체에 필요한 모든 정보(사용자 식별, 권한 등)를 담고 있다. 따라서 서버가 사용자 세션 정보를 별도로 저장하고 관리할 필요가 없다.
JWT의 구조
JWT는 .
을 구분자로 하여 Header, Payload, Signature 세 부분으로 구성된다.
xxxxx.yyyyy.zzzzz
Header
JWT의 헤더는 JWT 자체에 대한 클레임(정보)을 포함하며, 사용된 알고리즘, 서명/암호화 여부, 파싱 방법을 정의한다.
{
"alg": "HS256",
"typ": "JWT", // optional
"cty": "JWT" // optional
}
- 필수 헤더 정보
- alg: JWT의 서명/암호화에 사용된 알고리즘. 암호화되지 않은 JWT의 경우 값은 none으로 설정한다.
- 선택적 헤더 정보
- typ: JWT의 미디어 타입. 다른 JWT 헤더 객체와 혼합될 경우 구분할 때 사용한다. 보통 JWT로 설정되며, 실제 사용은 드물다.
- cty: 콘텐츠 타입. 페이로드가 일반 클레임과 데이터를 포함하면 설정되지 않는다. 페이로드가 중첩된 JWT일 경우 JWT로 설정되어 추가 처리가 필요함을 알림. 중첩 JWT는 드물어 cty도 거의 사용되지 않는다.
Payload
토큰에 담을 정보를 포함한다. 이 정보 조각들을 클레임(Claim)이라고 부른다.
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1516239022
}
Registered Claims
JWT 사양에 이미 정의된 클레임들이다. 필수는 아니지만 권장되는 set이다.
- iss (issuer): JWT를 발행한 주체를 식별하는 고유 문자열 또는 URI.
- exp (expiration): JWT가 만료되는 시점을 POSIX 형식으로 나타낸 숫자.
- sub (subject): JWT가 정보를 담고 있는 주체를 식별하는 고유 문자열 또는 URI.
- aud (audience): JWT의 의도된 수신자를 식별하는 문자열, URI, 또는 배열.
- nbf (not before): JWT가 유효해지는 시작 시점을 POSIX 형식으로 나타낸 숫자.
- iat (issued at): JWT가 발행된 시점을 POSIX 형식으로 나타낸 숫자.
- jti (JWT ID): JWT의 고유 식별자 문자열.
Public and Private Claims
- Public: JWT를 사용하는 사람들끼리 충돌을 방지하기 위해 공개적으로 정의된 클레임이다.
- Private: 서버와 클라이언트 간에 협의 하에 사용하는 클레임이다. 사용자의 ID, 이름, 권한 등과 같은 정보를 여기에 담는다.
Signature
헤더와 페이로드 인코딩한 값과 서버가 가진 비밀 키(Secret Key)를 헤더에 명시된 알고리즘으로 암호화하여 생성한다. 예를 들어 HMAC SHA256 알고리즘을 사용한 경우 다음과 같이 시그니처를 만든다.
HMACSHA256(
Base64UrlEncode(header) + "." + Base64UrlEncode(payload),
secret
)
이 서명은 토큰의 무결성을 보장하는 역할을 한다.
합치기
이제 세 가지 값들을 Base64Url 로 인코딩하여 구분자(.
)를 통해 하나의 문자열로 합치면 JWT가 만들어진다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30
JWT의 인증 과정
JWT가 어떤 정보를 담고 있는지 구조에 대해 알아봤으니, 이제 실제로 어떤 순서로 동작하는지 살펴보자.
Note
아래 과정은 HS256(대칭키) 알고리즘을 기준으로 설명하여, 다른 알고리즘 사용시 과정이 다를 수 있다.
- 사용자가 로그인에 성공하면, 서버는 사용자의 정보를 담은 JWT를 생성하여 클라이언트에게 전달한다.
- 클라이언트(브라우저)가 발급받은 JWT를 저장소(로컬스토리지, 세션스토리지 쿠키 등)에 저장한다.
- 이후 서버에 요청을 보낼 때마다 헤더에 JWT를 담아 보낸다.
- 서버는 전달받은 JWT의 서명을 검증하여 토큰의 유효성과 변조 여부를 확인한다.
- 검증이 완료되면, 서버는 토큰에 담긴 사용자 정보를 신뢰하고 요청을 처리한다.
어떻게 서명을 검증할까?
서버가 클라이언트로부터 JWT(xxxxx.yyyyy.zzzzz
)를 받으면 다음과 같은 절차를 따르게 된다.
-
토큰 분리: 서버는 먼저 전달받은 JWT를
.
을 기준으로 헤더(Header), 페이로드(Payload), 서명(Signature) 세 부분으로 분리한다. -
헤더와 페이로드 준비: 분리된 헤더와 페이로드를 준비합니다. 이 두 부분은 토큰을 생성할 때와 마찬가지로 Base64Url로 인코딩된 상태이다.
-
서명 재생성: 서버는 자신만이 안전하게 보관하고 있는 비밀 키를 사용하여 서명을 다시 만든다.
javascriptHMACSHA256( Base64UrlEncode(header) + "." + Base64UrlEncode(payload), secret )
-
서명 비교: 서버가 직접 만든 서명과 클라이언트가 보낸 JWT에 포함된 기존 서명을 비교한다.
JWT의 장단점
장점
- Stateless 및 확장성: 서버가 사용자의 상태를 저장하지 않으므로, 서버의 부하를 줄이고 수평적으로 확장하기 용이하다.
- 플랫폼 독립성: 토큰 기반이므로 모바일, 웹, 데스크톱 등 다양한 플랫폼과 디바이스에서 동일하게 작동한다.
- 보안: 서명을 통해 토큰의 변조 여부를 검증할 수 있다.
단점
- 토큰 길이: 세션 ID에 비해 토큰의 길이가 깁니다. 이는 네트워크 트래픽에 약간의 오버헤드를 유발할 수 있다.
- 보안 민감 정보 저장 불가: 페이로드는 Base64로 인코딩될 뿐, 암호화되지 않습니다. 누구나 디코딩하여 내용을 확인할 수 있으므로, 비밀번호와 같은 민감한 정보는 담아서는 안 된다.
- 토큰 무효화의 어려움: JWT는 한 번 발급되면 만료 시간 전까지는 계속 유효하다. 서버가 상태를 관리하지 않기 때문에, 만약 토큰이 탈취되더라도 서버에서 강제로 무효화하기 어렵다.
마무리
그저 "다들 쓰니까" 사용해왔던 JWT. 하지만 누군가에게 이 기술을 명확히 설명하기 위해 그 탄생 배경부터 동작 방식에 이르기까지 다시 한번 되짚어보는 시간을 가졌다. 이 글을 읽는 다른 누군가에게도 JWT에 대해 다시 알아볼 수 있는 시간이 되었으면 좋겠다.