본문 바로가기
Computer Science

JWT(JSON Web Token)의 이해와 활용: 1편

by 대박플머 2024. 10. 14.

안녕하세요, 오늘은 현대 웹 개발에서 중요한 역할을 하는 JWT(JSON Web Token)에 대해 자세히 알아보겠습니다. 이 글에서는 JWT의 기본 개념부터 구조, 그리고 실제 사용 방법까지 다룰 예정입니다.

1. JWT란 무엇인가?

JWT의 정의 및 개요

JWT는 JSON Web Token의 약자로, 당사자 간에 정보를 안전하게 전송하기 위한 개방형 표준(RFC 7519)입니다. 이 토큰은 JSON 객체로 인코딩되어 있으며, 디지털 서명이 되어 있어 신뢰성을 보장합니다.

JWT는 주로 인증(Authentication)과 정보 교환에 사용됩니다. 웹 애플리케이션에서 사용자가 로그인하면, 서버는 JWT를 생성하여 클라이언트에게 전달합니다. 이후 클라이언트는 이 토큰을 사용하여 서버에 요청을 보낼 때마다 자신의 신원을 증명합니다.

JWT의 탄생 배경 (Auth0와 RFC 7519)

JWT의 개념은 Auth0라는 회사에서 처음 제안되었습니다. Auth0는 개발자들이 인증과 권한 부여를 쉽게 구현할 수 있도록 돕는 플랫폼을 제공하는 회사입니다. 그들은 기존의 인증 방식들이 가진 한계를 극복하고자 JWT를 고안했습니다.

2015년 5월, JWT는 IETF(Internet Engineering Task Force)에 의해 RFC 7519로 표준화되었습니다. 이로써 JWT는 공식적인 웹 표준이 되었고, 많은 개발자들이 이를 채택하기 시작했습니다.

다른 인증 방식과의 비교 (세션 기반 인증과의 차이)

JWT를 이해하기 위해서는 기존의 세션 기반 인증과 비교해 보는 것이 도움이 됩니다.

  1. 세션 기반 인증:
    • 사용자가 로그인하면 서버에 세션을 생성하고 세션 ID를 클라이언트에게 전달합니다.
    • 클라이언트는 이 세션 ID를 쿠키에 저장하고, 매 요청마다 이를 서버에 전송합니다.
    • 서버는 세션 저장소에서 해당 세션 ID를 확인하여 사용자를 인증합니다.
  2. JWT 기반 인증:
    • 사용자가 로그인하면 서버는 JWT를 생성하여 클라이언트에게 전달합니다.
    • 클라이언트는 이 토큰을 저장하고 (주로 로컬 스토리지나 쿠키), 매 요청마다 이를 헤더에 포함하여 서버에 전송합니다.
    • 서버는 토큰의 서명을 확인하여 사용자를 인증합니다.

주요 차이점:

  • 상태 저장 vs 무상태: 세션 기반 인증은 서버에 상태를 저장해야 하지만, JWT는 무상태(stateless)입니다.
  • 확장성: JWT는 서버 측 세션 저장소가 필요 없어 더 쉽게 확장할 수 있습니다.
  • 보안: 세션 ID는 의미 없는 문자열이지만, JWT는 자체적으로 정보를 포함하고 있어 주의가 필요합니다.

2. JWT의 구조

JWT는 세 부분으로 구성되어 있으며, 각 부분은 점(.)으로 구분됩니다. 구조는 다음과 같습니다:

xxxxx.yyyyy.zzzzz

여기서 각 부분은 다음을 나타냅니다:

  • xxxxx: Header (헤더)
  • yyyyy: Payload (페이로드)
  • zzzzz: Signature (서명)

JWT의 기본 구성 요소

  1. Header (헤더):
    헤더는 주로 두 가지 정보를 담고 있습니다:
    • 토큰의 유형 (typ): JWT
    • 사용된 해싱 알고리즘 (alg): 예를 들어, HMAC SHA256 또는 RSA
    예시:{ "alg": "HS256", "typ": "JWT" }
  2. Payload (페이로드):
    페이로드에는 클레임(claim)이라 불리는 엔티티에 대한 명세와 추가적인 데이터를 포함합니다. 클레임은 세 가지 유형이 있습니다:
    • 등록된 클레임: 미리 정의된 클레임. 예: iss (발행자), exp (만료 시간), sub (주제) 등
    • 공개 클레임: 사용자 정의 클레임. JWT를 사용하는 당사자들 간에 합의된 정보
    • 비공개 클레임: 당사자들 간에 정보를 공유하기 위해 생성된 맞춤 클레임
    예시:{ "sub": "1234567890", "name": "John Doe", "admin": true }
  3. Signature (서명):
    서명은 헤더의 인코딩 값과 페이로드의 인코딩 값을 합친 후, 비밀 키로 해싱하여 생성합니다. 이는 토큰이 변조되지 않았음을 확인하는 데 사용됩니다.
    HMACSHA256(
      base64UrlEncode(header) + "." +
      base64UrlEncode(payload),
      secret
    )

Base64 인코딩의 이해

JWT의 각 부분은 Base64Url 인코딩을 사용하여 인코딩됩니다. Base64Url은 일반적인 Base64 인코딩과 유사하지만, URL에서 안전하게 사용할 수 있도록 '+' 대신 '-', '/' 대신 '_'를 사용하고, 패딩 문자 '='를 제거합니다.

Base64 인코딩의 주요 목적은 바이너리 데이터를 텍스트 형식으로 변환하는 것입니다. JWT에서는 이를 통해 JSON 객체를 문자열로 변환하고, URL에서 안전하게 전송할 수 있게 합니다.

주의할 점은 Base64 인코딩은 암호화가 아니라는 것입니다. 따라서 JWT의 헤더와 페이로드는 누구나 디코딩하여 읽을 수 있습니다. 이 때문에 JWT에 민감한 정보를 포함시키지 않는 것이 중요합니다.

JWT의 동작 방식 (서명과 검증)

JWT의 동작 방식은 다음과 같습니다:

  1. 토큰 생성:
    • 서버는 헤더와 페이로드를 생성합니다.
    • 각 부분을 Base64Url로 인코딩합니다.
    • 인코딩된 헤더와 페이로드를 비밀 키를 사용하여 서명합니다.
    • 최종적으로 헤더, 페이로드, 서명을 점(.)으로 구분하여 하나의 문자열로 만듭니다.
  2. 토큰 전송:
    • 생성된 토큰을 클라이언트에게 전송합니다.
    • 클라이언트는 이 토큰을 저장하고, 이후 요청 시 Authorization 헤더에 포함시켜 서버에 전송합니다.
  3. 토큰 검증:
    • 서버는 받은 토큰을 점(.)을 기준으로 분리합니다.
    • 헤더와 페이로드를 사용하여 서명을 재생성합니다.
    • 재생성된 서명과 토큰의 서명 부분을 비교합니다.
    • 서명이 일치하면 토큰이 변조되지 않았음을 확인할 수 있습니다.
    • 추가로 토큰의 만료 시간 등을 확인하여 유효성을 검사합니다.

이러한 방식으로 JWT는 토큰의 무결성을 보장하고, 서버는 클라이언트의 신원을 확인할 수 있습니다.

3. JWT 생성 및 검증

JWT 생성 과정 (서명 방식: HMAC, RSA 등)

JWT를 생성할 때는 주로 두 가지 서명 방식을 사용합니다:

  1. HMAC (Hash-based Message Authentication Code):
    • 대칭 키 암호화 방식입니다.
    • 하나의 비밀 키를 사용하여 서명을 생성하고 검증합니다.
    • 구현이 간단하고 빠르지만, 키 관리에 주의가 필요합니다.
  2. RSA (Rivest–Shamir–Adleman):
    • 비대칭 키 암호화 방식입니다.
    • 공개 키와 개인 키 쌍을 사용합니다.
    • 개인 키로 서명을 생성하고, 공개 키로 검증합니다.
    • 키 관리가 더 복잡하지만, 보안성이 높습니다.

JWT 생성 과정:

  1. 헤더와 페이로드 객체를 생성합니다.
  2. 각 객체를 JSON으로 직렬화합니다.
  3. JSON 문자열을 Base64Url로 인코딩합니다.
  4. 인코딩된 헤더와 페이로드를 점(.)으로 연결합니다.
  5. 선택한 알고리즘(HMAC 또는 RSA)과 비밀 키를 사용하여 서명을 생성합니다.
  6. 서명을 Base64Url로 인코딩합니다.
  7. 인코딩된 헤더, 페이로드, 서명을 점(.)으로 연결하여 최종 JWT를 생성합니다.

JWT 검증 과정 (무결성 확인 및 유효성 검증)

JWT를 받은 서버는 다음과 같은 과정을 통해 토큰을 검증합니다:

  1. JWT를 점(.)을 기준으로 헤더, 페이로드, 서명으로 분리합니다.
  2. 헤더를 디코딩하여 사용된 알고리즘을 확인합니다.
  3. 헤더와 페이로드를 사용하여 서명을 재생성합니다.
  4. 재생성된 서명과 받은 서명을 비교합니다.
  5. 서명이 일치하면 토큰의 무결성이 확인됩니다.
  6. 페이로드를 디코딩하여 클레임을 확인합니다.
  7. 만료 시간(exp), 발행 시간(iat) 등의 클레임을 검사하여 토큰의 유효성을 확인합니다.
  8. 필요에 따라 추가적인 클레임(예: 발행자, 대상자 등)을 검증합니다.

JWT의 유효 기간 설정과 관리

JWT의 유효 기간 설정은 보안과 사용자 경험 사이의 균형을 맞추는 중요한 요소입니다.

  1. 유효 기간 설정:
    • 'exp' (Expiration Time) 클레임을 사용하여 토큰의 만료 시간을 지정합니다.
    • 일반적으로 짧은 유효 기간(예: 15분~1시간)을 설정하여 보안을 강화합니다.
  2. 리프레시 토큰:
    • 짧은 유효 기간의 단점을 보완하기 위해 리프레시 토큰을 사용할 수 있습니다.
    • 리프레시 토큰은 더 긴 유효 기간을 가지며, 새로운 액세스 토큰을 발급받는 데 사용됩니다.
  3. 토큰 갱신:
    • 클라이언트는 액세스 토큰이 만료되기 전에 리프레시 토큰을 사용하여 새 액세스 토큰을 요청합니다.
    • 서버는 리프레시 토큰의 유효성을 확인하고 새로운 액세스 토큰을 발급합니다.
  4. 토큰 폐기:
    • JWT는 기본적으로 무상태이므로 서버 측에서 직접 폐기할 수 없습니다.
    • 필요한 경우, 블랙리스트를 유지하여 특정 토큰을 무효화할 수 있습니다.
    • 또는 모든 토큰에 고유 식별자를 포함시키고, 서버에서 유효한 식별자 목록을 관리할 수 있습니다.

예시 코드 (Node.js/Express를 사용한 JWT 생성 및 검증)

Node.js와 Express 프레임워크를 사용하여 JWT를 생성하고 검증하는 간단한 예제를 살펴보겠습니다. 이 예제에서는 jsonwebtoken 라이브러리를 사용합니다.

먼저, 필요한 패키지를 설치합니다:

npm install express jsonwebtoken

그리고 다음과 같이 코드를 작성합니다:

const express = require("express");
const jwt = require("jsonwebtoken");

const app = express();
const secretKey = "your-secret-key";

// JWT 생성
app.post("/login", (req, res) => {
  // 여기서는 사용자 인증 로직을 생략하고 바로 토큰을 생성합니다
  const user = { id: 1, username: "example" };

  const token = jwt.sign(user, secretKey, { expiresIn: "1h" });

  res.json({ token });
});

// JWT 검증 미들웨어
function authenticateToken(req, res, next) {
  const authHeader = req.headers["authorization"];
  const token = authHeader && authHeader.split(" ")[1];

  if (token == null) return res.sendStatus(401);

  jwt.verify(token, secretKey, (err, user) => {
    if (err) return res.sendStatus(403);
    req.user = user;
    next();
  });
}

// 보호된 라우트
app.get("/protected", authenticateToken, (req, res) => {
  res.json({ message: "보호된 데이터에 접근했습니다", user: req.user });
});

app.listen(3000, () => {
  console.log("서버가 3000번 포트에서 실행 중입니다");
});

이 예제에서:

  1. /login 라우트에서 JWT를 생성합니다. 실제 애플리케이션에서는 사용자 인증 후에 토큰을 생성해야 합니다.
  2. authenticateToken 미들웨어는 요청 헤더에서 토큰을 추출하고 검증합니다.
  3. /protected 라우트는 인증된 사용자만 접근할 수 있습니다.

이 코드를 실행하면, 클라이언트는 다음과 같이 API를 사용할 수 있습니다:

  1. /login에 POST 요청을 보내 토큰을 받습니다.
  2. 받은 토큰을 Authorization 헤더에 "Bearer [token]" 형식으로 포함시켜 /protected에 GET 요청을 보냅니다.

이러한 방식으로 JWT를 사용하면 서버는 세션을 유지하지 않고도 클라이언트의 인증 상태를 확인할 수 있습니다.

결론

이번 글에서는 JWT의 기본 개념, 구조, 그리고 생성 및 검증 과정에 대해 살펴보았습니다. JWT는 현대 웹 개발에서 널리 사용되는 인증 방식으로, 특히 분산 시스템이나 마이크로서비스 아키텍처에서 유용합니다.

JWT의 주요 장점은 다음과 같습니다:

  • 서버의 상태를 유지할 필요가 없어 확장성이 좋습니다.
  • 다양한 프로그래밍 언어에서 지원되어 호환성이 높습니다.
  • 토큰 자체에 정보를 포함할 수 있어 유연합니다.

그러나 JWT를 사용할 때는 다음 사항에 주의해야 합니다:

  • 토큰의 페이로드는 암호화되지 않으므로 민감한 정보를 포함하지 않아야 합니다.
  • 적절한 유효 기간 설정과 토큰 관리가 중요합니다.
  • 클라이언트 측 저장소(예: 로컬 스토리지)에 토큰을 저장할 때는 보안에 주의해야 합니다.

JWT를 올바르게 이해하고 사용한다면, 안전하고 효율적인 인증 시스템을 구축할 수 있습니다. 다음 편에서는 JWT의 고급 주제와 실제 애플리케이션에서의 활용 방안에 대해 더 자세히 다루도록 하겠습니다.