이전 프로젝트에서 JWT 토큰을 사용했는데 돌이켜보니 토큰에 대한 이해가 부족한 것 같아 가볍게 정리하였습니다.
JWT란? Json Web Token
의 약자로 서버와 클라이언트 사이에서 사용자를 인증하고 권한을 부여하는데 사용하는 토큰(문자열)입니다. 이러한 토큰을 사용하는 이유는 HTTP의 비연결성 때문에 서버는 클라이언트에서 온 요청을 어떤 누가 보냈는지 알 수 없기 때문에 쿠키/세션 방식이나 토큰방식을 사용해서 어떤 유저인지 식별합니다.
JWT는 Header, Payload, Signature로 3등분되어 있습니다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYzOTZlODNmZDQzc2RnZHMxMXNhZGQiLCJleHAiOjE2ODk2MDY4MTUsImlhdCI6MTY4NDQyMjgxNX0.MePqD1GmA4EK4jNhose3zdTZ3dvfIGd2-G1L1dNn3sE
각 부위는 .
으로 나누어져 있으며 꼭 JWT관련 사이트가 아니더라도 Base64를 구글링하면 나오는 디코드 사이트를 통해 디코딩하여 값을 확인해볼 수 있습니다.
이처럼 Header, Payload 부분은 base64로 단순 인코딩된것이며 어느 누구나 문자열로 변환할 수 있기 때문에 중요하거나 민감한 정보를 넣어서는 안됩니다.
{
"alg": "HS256",
"typ": "JWT"
}
Header는 두가지 키값을 가지고 있습니다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYzOTZlODNmZDQzc2RnZHMxMXNhZGQiLCJleHAiOjE2ODk2MDY4MTUsImlhdCI6MTY4NDQyMjgxNX0.MePqD1GmA4EK4jNhose3zdTZ3dvfIGd2-G1L1dNn3sE
{
"id": "1a2b3c4d5",
"exp": 1689606815,
"iat": 1684422815
}
Payload 에는 다양한 값이 담길 수 있는데 여기 담기는 값들을 클레임(claim)이라고 부릅니다. name/value로 이루어져 있습니다.
다음은 서비스에 대한 정보가 아닌, 토큰에 대한 정보를 담기위한 클레임들입니다. 모두 사용하는것은 아니고 선택적으로 사용됩니다.
공개 클레임이란 사용자 정의 클레임으로 공개용 정보를 위해 사용되며 충돌 방지를 위해 URI 포맷 또는 UUID를 사용해서 만듭니다.
{
"https://velog.io" : true
}
공개 클레임과 마찬가지로 사용자 정의 클레임으로 서버와 클라이언트 사이에 협의한 데이터를 저장합니다.
{
"name": "John Doe",
"email": "john.doe@example.com",
"role": "user"
}
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYzOTZlODNmZDQzc2RnZHMxMXNhZGQiLCJleHAiOjE2ODk2MDY4MTUsImlhdCI6MTY4NDQyMjgxNX0.MePqD1GmA4EK4jNhose3zdTZ3dvfIGd2-G1L1dNn3sE
공개 클레임과 비공개 클레임 모두 커스텀 클레임인데 기준이 명확이 이해가 가지 않았는데 충돌방지 포멧으로 작성하면 공개 클레임 그 외에는 비공개 클레임이라는 글을 발견하였다.
https://stackoverflow.com/questions/49215866/what-is-difference-between-private-and-public-claims-on-jwt
시그니처는 Header와 Payload를 각각 Base64로 인코딩한 후 합쳐서 Header에 명시된 서명 알고리즘과 비밀키를 통해 해쉬값으로 만들어집니다. 만들어진 해쉬값을 Base64로 한번 더 인코딩하면 JWT에 담기는 시그니처가 됩니다. 위 에서는 HS256
를 사용했습니다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYzOTZlODNmZDQzc2RnZHMxMXNhZGQiLCJleHAiOjE2ODk2MDY4MTUsImlhdCI6MTY4NDQyMjgxNX0.MePqD1GmA4EK4jNhose3zdTZ3dvfIGd2-G1L1dNn3sE
비밀키를 기반으로 서명 알고리즘이 해쉬값을 생성하기 때문에 예측가능하거나 노출되면 안됩니다.
이렇게 만들어진 JWT를 서버에서는 다음과 같은 절차로 검증합니다.
토큰 파싱: 서버는 수신한 JWT를 파싱하여 헤더, 페이로드, 서명으로 분리합니다.
서명 검증: 서버는 헤더와 페이로드, 비밀 키를 사용하여 수신된 서명과 동일한 서명을 생성합니다. 이때 사용하는 알고리즘은 JWT의 헤더에 명시되어 있습니다. 만약 HS256 알고리즘을 사용한다면, 서버는 HMAC-SHA256 알고리즘을 사용하여 서명을 생성합니다.
유효성 검사: 서버는 생성한 서명과 수신된 서명이 일치하는지 확인합니다. 일치하지 않는 경우 토큰은 변조되었거나 유효하지 않은 토큰일 수 있습니다.
추가 검증: 선택적으로, 서버는 토큰의 만료 시간(expiration time), 발급자(issuer), 토큰의 용도 등 추가적인 검증을 수행할 수 있습니다. 이러한 검증은 토큰의 신뢰성과 안전성을 높이기 위해 사용됩니다.
사실 JWT를 백엔드에서 제공할때 가져다 쓰고 명세에 따라서 헤더에 첨부해서 보내기만 했지 내부적으로 어떻게 구현되고 서버에서 어떻게 검증하는지는 고려해보지 않은 것 같습니다. 이번 기회에 다시 한번 짚고 넘어갈 수 있어서 다행이었습니다.
마지막으로 이런 복잡한 과정들을 간편하게 사용하기 위해서 보통 라이브러리를 많이 이용하는데, Node.js 에서는 JWT 표준 명세서를 따르는 jsonwebtoken
라이브러리를 주로 사용하는 것 같습니다.
Reference
https://jwt.io/
https://velopert.com/2389