홈으로 돌아가기

JWT 인증 FastAPI: 단계별 코드

중/시니어 개발자를 위한 FastAPI JWT 인증 구현 가이드. 토큰 구조, Pydantic 스키마, 보안 모듈, 의존성 및 보호된 엔드포인트에 대한 전체 코드 포함.

FastAPI의 JWT: 로그인 코드 + 보호된 라우트
Advertisement 728x90

FastAPI에서 JWT 인증 구현하기: 완벽한 개발자 가이드

JWT(JSON Web Token)는 당사자 간 클레임을 전송하기 위한 간결한 토큰입니다. 토큰은 헤더, 페이로드, 서명의 세 가지 Base64 인코딩 부분으로 구성되며, 마침표로 구분됩니다.

헤더는 서명 알고리즘(예: HS256)과 토큰 유형(JWT)을 포함합니다:

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

페이로드는 클레임을 저장합니다: sub(사용자 식별자), iat(발급 시간), exp(만료 시간):

Google AdInline article slot
{"sub": "alex", "name": "Alex Tomchok", "iat": 1516239022, "exp": 1516242622}

서명은 (헤더.페이로드 + 비밀 키)의 HMAC-SHA256으로 계산됩니다. 서버는 검증 중에 서명을 확인합니다: 페이로드의 변경은 토큰을 무효화합니다. 데이터는 읽을 수 있지만 수정으로부터 보호됩니다.

액세스 토큰은 짧은 수명(15–30분)을 가지며, 리프레시 토큰은 장기간(일/월) 유지됩니다. 이 예제는 액세스 토큰만 사용합니다.

프로젝트 설정 및 의존성

구조 생성:

Google AdInline article slot
fastapi-jwt-auth/
├── app/
│   ├── main.py
│   ├── core/
│   │   ├── config.py
│   │   └── security.py
│   ├── schemas/
│   │   ├── token.py
│   │   └── user.py
│   ├── api/
│   │   └── v1/
│   │       ├── endpoints/
│   │       │   ├── auth.py
│   │       │   └── users.py
│   │       └── dependencies.py
│   └── services/
│       └── user_service.py
└── requirements.txt

requirements.txt:

fastapi==0.104.1
uvicorn[standard]==0.24.0
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
python-multipart==0.0.6
pydantic[email]==2.5.0

설치: pip install -r requirements.txt.

설정 및 Pydantic 스키마

core/config.py에서 Pydantic을 사용하여 설정 정의:

Google AdInline article slot
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    SECRET_KEY: str = "your-super-secret-key-change-this-in-production"
    ALGORITHM: str = "HS256"
    ACCESS_TOKEN_EXPIRE_MINUTES: int = 30

    class Config:
        env_file = ".env"

settings = Settings()

schemas/user.py의 스키마:

from pydantic import BaseModel, EmailStr

class UserBase(BaseModel):
    username: str
    email: EmailStr

class UserCreate(UserBase):
    password: str

class User(UserBase):
    id: int
    is_active: bool = True

    class Config:
        from_attributes = True

class UserInDB(User):
    hashed_password: str

schemas/token.py:

from pydantic import BaseModel

class Token(BaseModel):
    access_token: str
    token_type: str = "bearer"

class TokenData(BaseModel):
    username: str | None = None

보안: 해싱 및 JWT 작업

core/security.py에서 비밀번호 해싱(bcrypt) 및 JWT 함수 구현:

from datetime import datetime, timedelta
from jose import JWTError, jwt
from passlib.context import CryptContext
from .config import settings

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def verify_password(plain_password: str, hashed_password: str) -> bool:
    return pwd_context.verify(plain_password, hashed_password)

def get_password_hash(password: str) -> str:
    return pwd_context.hash(password)

def create_access_token(data: dict) -> str:
    to_encode = data.copy()
    expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
    return encoded_jwt

def decode_access_token(token: str) -> dict:
    try:
        payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
        return payload
    except JWTError:
        return None

사용자 서비스 및 가짜 데이터베이스

메모리 저장소를 사용한 services/user_service.py:

from app.schemas.user import UserInDB
from app.core.security import verify_password, get_password_hash

fake_users_db = {
    "alex": {
        "id": 1,
        "username": "alex",
        "email": "[email protected]",
        "hashed_password": get_password_hash("secret123"),
        "is_active": True,
    }
}

def get_user_by_username(username: str) -> UserInDB | None:
    if username in fake_users_db:
        return UserInDB(**fake_users_db[username])
    return None

def authenticate_user(username: str, password: str) -> UserInDB | None:
    user = get_user_by_username(username)
    if not user:
        return None
    if not verify_password(password, user.hashed_password):
        return None
    return user

인증을 위한 의존성

api/v1/dependencies.py에서 OAuth2PasswordBearer 사용:

from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from app.core.security import decode_access_token
from app.services.user_service import get_user_by_username
from app.schemas.token import TokenData
from app.schemas.user import User

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")

async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="자격 증명을 확인할 수 없습니다",
        headers={"WWW-Authenticate": "Bearer"},
    )
    payload = decode_access_token(token)
    if payload is None:
        raise credentials_exception
    username: str = payload.get("sub")
    if username is None:
        raise credentials_exception
    user = get_user_by_username(username)
    if user is None:
        raise credentials_exception
    return user

async def get_current_active_user(current_user: User = Depends(get_current_user)) -> User:
    if not current_user.is_active:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="비활성 사용자")
    return current_user

엔드포인트: 로그인 및 보호된 경로

api/v1/endpoints/auth.py:

from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from app.schemas.token import Token
from app.services.user_service import authenticate_user
from app.core.security import create_access_token

router = APIRouter(prefix="/auth", tags=["authentication"])

@router.post("/login", response_model=Token)
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
    user = authenticate_user(form_data.username, form_data.password)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="사용자 이름 또는 비밀번호가 올바르지 않습니다",
            headers={"WWW-Authenticate": "Bearer"},
        )
    access_token = create_access_token(data={"sub": user.username})
    return Token(access_token=access_token, token_type="bearer")

api/v1/endpoints/users.py:

from fastapi import APIRouter, Depends
from app.schemas.user import User
from app.api.v1.dependencies import get_current_active_user

router = APIRouter(prefix="/users", tags=["users"])

@router.get("/me", response_model=User)
async def read_users_me(current_user: User = Depends(get_current_active_user)):
    return current_user

main.py에서 라우터 연결 및 uvicorn app.main:app 실행.

핵심 포인트

  • JWT 페이로드는 암호화되지 않고 서명만 됩니다 — 민감하지 않은 데이터만 사용하세요.
  • SECRET_KEY를 환경 변수에 저장하고, 32자 이상으로 생성하세요.
  • 프로덕션에서는 리프레시 토큰과 데이터베이스(SQLAlchemy + Alembic)를 추가하세요.
  • FastAPI 의존성을 통해 인증 로직을 중복 없이 재사용할 수 있습니다.
  • Swagger UI를 통해 엔드포인트 테스트: /docs.

— Editorial Team

Advertisement 728x90

다음 읽기