JWT 토큰 설명: 현대 웹 앱을 위한 인증

· 12분 읽기

목차

JSON Web Token(JWT)은 현대 웹 애플리케이션에서 인증의 사실상 표준이 되었습니다. 두 당사자 간에 클레임을 표현하는 컴팩트하고 URL 안전한 방법을 제공하여, 공유 세션 저장소 없이 수평적으로 확장 가능한 무상태 인증을 가능하게 합니다.

REST API, 마이크로서비스 아키텍처 또는 단일 페이지 애플리케이션을 구축하든, JWT를 이해하는 것은 안전하고 확장 가능한 인증을 구현하는 데 필수적입니다. 이 종합 가이드는 JWT의 작동 방식, 구조, 보안 고려사항 및 프로덕션 준비 모범 사례를 설명합니다.

JWT란 무엇인가?

JWT("jot"으로 발음)는 JSON 객체로 당사자 간에 정보를 안전하게 전송하기 위한 컴팩트하고 자체 포함된 형식을 정의하는 공개 표준(RFC 7519)입니다. 정보는 비밀 키(HMAC) 또는 공개/개인 키 쌍(RSA 또는 ECDSA)을 사용하여 디지털 서명되어 토큰의 무결성과 진위성을 보장합니다.

서버가 메모리나 데이터베이스에 세션 상태를 유지하는 전통적인 세션 기반 인증과 달리, JWT는 무상태입니다. 사용자의 신원을 확인하는 데 필요한 모든 정보가 토큰 자체에 포함되어 있습니다.

JWT의 주요 사용 사례

프로 팁: 개발 중에 토큰을 검사하고 검증하려면 JWT 디코더를 사용하세요. 구조를 이해하고 프로덕션에 도달하기 전에 일반적인 문제를 파악하는 데 도움이 됩니다.

JWT 구조 분석

JWT는 점(.)으로 구분된 세 가지 고유한 부분으로 구성됩니다: 헤더.페이로드.서명

완전한 JWT는 다음과 같습니다:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkphbmUgRGV2ZWxvcGVyIiwiZW1haWwiOiJqYW5lQGV4YW1wbGUuY29tIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzEwNTkwNDAwLCJleHAiOjE3MTA2NzY4MDB9.4Adcj0mYZ8s5vxjKvV8pF7jKX9s8vZ5xJ3kL9mN2pQ4

파트 1: 헤더

헤더는 일반적으로 토큰의 암호화 작업을 설명하는 두 개의 필드를 포함합니다:

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

이 JSON 객체는 Base64Url로 인코딩되어 JWT의 첫 번째 부분을 형성합니다. 인코딩은 HTTP 헤더에서 전송하기 위해 URL 안전하고 컴팩트하게 만듭니다.

파트 2: 페이로드

페이로드는 클레임(사용자 및 추가 메타데이터에 대한 진술)을 포함합니다. 클레임은 세 가지 유형으로 분류됩니다:

{
  "sub": "1234567890",
  "name": "Jane Developer",
  "email": "[email protected]",
  "role": "admin",
  "iat": 1710590400,
  "exp": 1710676800
}

등록된 클레임은 JWT 사양에 의해 미리 정의되며 표준 정보를 제공합니다:

공개 클레임은 IANA JSON Web Token 레지스트리에 정의되거나 충돌 방지 이름(예: URL)을 사용해야 하는 사용자 정의 클레임입니다.

비공개 클레임role, permissions 또는 organizationId와 같이 당사자 간에 합의된 사용자 정의 클레임입니다.

중요: 페이로드는 Base64Url로 인코딩될 뿐 암호화되지 않습니다. 비밀번호, 신용카드 번호 또는 주민등록번호와 같은 민감한 정보를 JWT 페이로드에 절대 저장하지 마세요.

파트 3: 서명

서명은 토큰이 변경되지 않았음을 보장합니다. 인코딩된 헤더, 인코딩된 페이로드, 비밀 키를 가져와 헤더에 지정된 알고리즘을 적용하여 생성됩니다:

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

서명을 통해 수신자는 발신자가 주장하는 사람이 맞고 메시지가 도중에 변경되지 않았음을 확인할 수 있습니다.

서명 알고리즘 비교

알고리즘 유형 보안 수준 사용 사례 키 관리
HS256 대칭(HMAC) 좋음 단일 서비스, 내부 API 공유 비밀
RS256 비대칭(RSA) 매우 좋음 다중 서비스, 공개 API 공개/개인 키 쌍
ES256 비대칭(ECDSA) 우수 높은 보안 요구사항 공개/개인 키 쌍
PS256 비대칭(RSA-PSS) 우수 현대적인 애플리케이션 공개/개인 키 쌍

JWT 인증 작동 방식

전체 인증 흐름을 이해하면 JWT를 올바르게 구현하고 문제를 효과적으로 해결하는 데 도움이 됩니다.

전체 인증 흐름

  1. 사용자 로그인: 클라이언트가 인증 엔드포인트로 자격 증명(사용자 이름/비밀번호)을 보냅니다
  2. 자격 증명 확인: 서버가 데이터베이스에 대해 자격 증명을 검증합니다
  3. 토큰 생성: 인증 성공 시 서버는 사용자 정보와 클레임을 포함하는 JWT를 생성합니다
  4. 토큰 전달: 서버가 JWT를 클라이언트에 다시 보냅니다(일반적으로 응답 본문에)
  5. 토큰 저장: 클라이언트가 JWT를 저장합니다(메모리, localStorage 또는 httpOnly 쿠키에)
  6. 인증된 요청: 후속 요청에 대해 클라이언트는 Authorization 헤더에 JWT를 포함합니다
  7. 토큰 검증: 서버가 JWT 서명을 검증하고 만료를 확인합니다
  8. 액세스 허용: 유효한 경우 서버는 토큰의 클레임을 사용하여 요청을 처리합니다

인증 요청 예시

POST /api/auth/login HTTP/1.1
Host: api.example.com
Content-Type: application/json

{
  "email": "[email protected]",
  "password": "securePassword123"
}

인증 응답 예시

HTTP/1.1 200 OK
Content-Type: application/json

{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "expiresIn": 3600,
  "tokenType": "Bearer"
}

인증된 API 요청 예시

GET /api/users/profile HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

JWT vs 세션 기반 인증

JWT와 세션 기반 인증 중 선택하는 것은 애플리케이션의 아키텍처, 규모 및 요구사항에 따라 다릅니다.

측면 JWT 세션 기반
상태 무상태(서버가 토큰을 저장하지 않음) 상태 유지(서버가 세션 데이터를 저장)
확장성 우수(공유 저장소 불필요) 세션 저장소 필요(Redis, 데이터베이스)
취소 어려움(블랙리스트 또는 짧은 만료 필요) 쉬움(저장소에서 세션 삭제)
크기 더 큼(모든 요청과 함께 전송) 더 작음(세션 ID만)
크로스 도메인 쉬움(CORS 친화적) 복잡함(쿠키 도메인 제한)
모바일 앱 이상적(쿠키 지원 불필요) 어려움(쿠키 처리)
보안 신중한 구현 필요 잘 확립된 패턴

JWT를 사용해야 하는 경우

세션을 사용해야 하는 경우

애플리케이션에 JWT 구현하기

인기 있는 프로그래밍 언어와 프레임워크의 실용적인 구현 예제를 살펴보겠습니다.

Express를 사용한 Node.js

const jwt = require('jsonwebtoken');
const express = require('express');
const app = express();

const SECRET_KEY = process.env.JWT_SECRET;

// 로그인 엔드포인트
app.post('/api/auth/login', async (req, res) => {
  const { email, password } = req.body;
  
  // 자격 증명 확인(의사 코드)
  const user = await verifyCredentials(email, password);
  
  if (!user) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  
  // JWT 생성
  const token = jwt.sign(
    {
      sub: user.id,
      email: user.email,
      role: user.role
    },
    SECRET_KEY,
    { expiresIn: '1h' }
  );
  
  res.json({ accessToken: token });
});

// 인증 미들웨어
const authenticateToken = (req, res, next) => {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];
  
  if (!token) {
    return res.status(401).json({ error: 'No token provided' });
  }
  
  jwt.verify(token, SECRET_KEY, (err, user) => {
    if (err) {
      return res.status(403).json({ error: 'Invalid token' });
    }
    req.user = user;
    next();
  });
};

// 보호된 라우트
app.get('/api/users/profile', authenticateToken, (req, res) => {
  res.json({ user: req.user });
});

Flask를 사용한 Python

from flask import Flask, request, jsonify
import jwt
import datetime
import os

app = Flask(__name__)
SECRET_KEY = os.getenv('JWT_SECRET')

@app.route('/api/auth/login', methods=['POST'])
def login():
    data = request.get_json()
    email = data.get('email')
    password = data.get('password')
    
    # 자격 증명 확인
    user = verify_credentials(email, password)
    
    if not user:
        return jsonify({'error': 'Invalid credentials'}), 401
    
    # JWT 생성
    token = jwt.encode({
        'sub': user['id'],
        'email': user['email'],
        'role': user['role'],
        'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1)
    }, SECRET_KEY, algorithm='HS256')
    
    return jsonify({'accessToken': token})

def token_required(f):
    def decorator(*args, **kwargs):
        token = request.headers.get('Authorization')
        
        if not token:
            return jsonify({'error': 'No token provided'}), 401
        
        try:
            token = token.split(' ')[1]
            data = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
            request.user = data
        except jwt.ExpiredSignatureError:
            return jsonify({'error': 'Token expired'}), 403
        except jwt.InvalidTokenError:
            return jsonify({'error': 'Invalid token'}), 403
        
        return f(*args, **kwargs)
    
    decorator.__name__ = f.__name__
    return decorator

@app.route('/api/users/profile')
@token_required
def profile():
    return jsonify({'user': request.user})

빠른 팁: 비밀 키에는 항상 환경 변수를 사용하세요. 소스 코드에 하드코딩하거나 버전 관리에 커밋하지 마세요. 강력한 비밀을 생성하려면 비밀번호 생성기와 같은 도구를 사용하세요.

We use cookies for analytics. By continuing, you agree to our Privacy Policy.