seungjun.dev

JWT (JSON Web Token)

JWT 정리

인증된 사용자의 정보(JSON 객체)를 담아 암호화한 웹 토큰

간단히 말해, 사용자가 로그인을 성공적으로 마쳤을 때 서버가 발급해 주는 디지털 신분증과 같다.

클라이언트는 이 신분증(토큰)을 서버에 요청을 보낼 때마다 함께 제시해, 자신이 인증된 사용자라는 것을 증명한다. 서버는 이 신분증이 위조되지 않았는지 확인하고 요청을 처리한다.

구조

세 개의 파트가 .으로 구분된 긴 문자열 형태를 가진다.

xxxx.yyyy.zzzz (Header.Payload.Signature)

토큰의 **유형(typ)**과 어떤 **해싱 알고리즘(alg)**으로 서명되었는지를 담고 있다. Base64Url 방식으로 인코딩되어 첫 번째 부분을 구성한다.

{
  "alg": "HS256",
  "typ": "JWT"
}

Payload

토큰에 담을 실제 정보 조각들, 즉 **클레임(Claim)**이 들어가는 부분이다.

사용자의 ID, 이름 등 서버와 클라이언트가 주고받기로 약속한 정보들이 여기에 담긴다.

이 또한 Base64Url 방식으로 인코딩되어 두 번째 부분을 구성한다.

  • 중요: 페이로드는 암호화된 것이 아니라 인코딩된 것이다. 누구나 디코딩하여 내용을 볼 수 있으므로, 비밀번호와 같은 민감한 정보는 절대 넣으면 안 된다.
{
  "userId": "jsj9620",
  "username": "seungjun",
  "exp": 1725450177 // 토큰 만료 시간 (exp: expiration time)
}

Signature

이 토큰이 서버에서 발급한 진짜 토큰이 맞는지, 중간에 누군가 수정하지 않았는지를 증명하는 가장 중요한 부분이다.

서명은 다음과 같은 방식으로 생성된다.

HMACSHA256(Base64UrlEncode(Header) + "." + Base64UrlEncode(Payload), secret)

쉽게 말해, 인코딩된 헤더와 페이로드를 합친 후, 서버만 알고 있는 비밀 키로 해싱하여 생성한다.

클라이언트나 다른 사람은 이 비밀 키를 모르기 때문에 서명을 위조할 수 없다.

인증 과정

  1. 로그인 요청

    • 사용자가 ID와 비밀번호로 서버에 로그인 요청
  2. 서버 검증 및 토큰 발급

    • 서버는 DB와 대조하여 정보가 일치하면, 사용자의 정보를 담아 비밀 키로 서명한 JWT를 생성하여 클라이언트에게 응답으로 전달
  3. 토큰 저장

    • 클라이언트(브라우저)는 전달받은 토큰을 로컬 스토리지, 세션 스토리지, 또는 쿠키와 같은 곳에 저장
  4. 인증 요청

    • 이후 클라이언트는 API에 접근할 때마다, HTTP 요청의 Authorization 헤더에 이 토큰을 "Bearer"라는 접두사와 함께 담아 보냄
    • Authorization: Bearer xxxx.yyyy.zzzz
  5. 서버의 토큰 검증

    • 서버는 요청 헤더에 담긴 토큰을 받아, 자신이 가진 비밀 키를 사용해 서명을 다시 계산
    • 클라이언트가 보낸 토큰의 서명과 서버가 계싼한 서명이 일치하면, 요청을 처리

JWT의 저장

JWT는 로컬 스토리지에 저장하면 코드 한 줄 만으로 간편하게 저장할 수 있다. 그러나 이는 XSS (Cross-Site Scripting) 공격에 매우 취약하다.

XSS 공격

  1. 원인: JS로 접근 가능

    • 로컬 스토리지는 window 객체에 포함되어 있어, 어떤 JS 코드로든 localStorage.getItem('token')과 같은 명령어로 쉽게 접근하고 내용을 가져올 수 있음
  2. 공격 시나리오

    • 공격자가 웹사이트의 취약점(예: 댓글 창, 게시판)을 이용해 악성 JS 코드를 심어놓음
    • 다른 사용자가 해당 페이지를 방문하면, 브라우저는 이 악성 스크립트를 웹사이트의 정상 스크립트처럼 그대로 실행
    • 악성 스크립트는 사용자의 로컬 스토리지에 접근해 JWT를 훔친 뒤, 공격자의 서버로 몰래 전송
    • 공격자는 훔친 토큰을 이용해 개인정보를 빼가거나 중요한 데이터를 조작할 수 있음

한마디로, 아파트 현관문 비밀번호를 포스트잇에 적어 문에 붙여놓는 것과 같다.

해결 방법

JWT를 HttpOnly 속성을 적용한 쿠키에 저장하면 된다.

  1. HttpOnly란?

    • 쿠키에 HttpOnly 속성을 설정하는 것
    • 오직 서버와 브라우저 간의 HTTP 통신으로만 해당 쿠키를 사용할 수 있다
    • JS에서는 접근이 불가능해진다
  2. 동작 방식

    • 사용자가 로그인에 성공하면, 서버는 응답 헤더에 JWT를 담은 쿠키를 설정해서 보낸다.
      • 이때 HttpOnly 옵션을 반드시 포함한다.
      • Set-Cookie: accessToken=xxxxx.yyyyy.zzzzz; HttpOnly; Secure; SameSite=Strict
        • 추가 보안 옵션
          • Secure: HTTPS 프로토콜을 통해서만 쿠키가 전송되도록 하여 중간자 공격(MITM)을 방지
          • SameSite=Strict 또는 Lax: 다른 도메인에서 요청이 시작될 때 쿠키 전송을 제한하여 CSRF(Cross-Site Request Forgery) 공격을 방지
    • 브라우저는 이 쿠키를 안전한 곳에 저장하고, 이후 서버에 요청을 보낼 때마다 자동으로 쿠키를 헤더에 담아 전송한다.
    • JS는 이 쿠키의 존재 자체를 알 수 없으므로, XSS 공격 스크립트가 실행되더라도 토큰을 훔칠 수 없다.

핵심 장점

무상태성

토큰 안에 모든 정보가 담겨있고, 서버는 토큰의 유효성만 검증하면 되기 때문에 로그인 상태를 별도로 저장하지 않아도 된다.

확장성

서버가 상태를 저장하지 않으므로, 여러 대의 서버를 두어도 사용자는 어떤 서버에 요청하든 상관없이 인증을 유지할 수 있다.

유연성

웹뿐만 아니라 모바일 앱 등 다양한 플랫폼에서 사용하기 편리하며, CORS 문제도 비교적 쉽게 해결할 수 있다.