FastAPI 서버개발, OAuth2와 JWT를 사용한 애플리케이션 보안

현대 웹 애플리케이션은 다양한 보안 위협에 노출되어 있으며, 사용자 인증 및 권한 부여는 이러한 위협으로부터 애플리케이션을 보호하는 데 필수적입니다. 본 블로그에서는 FastAPI를 사용하여 OAuth2 프로토콜과 JWT(JSON Web Token)를 통한 인증 및 권한 부여 시스템을 구축하는 방법을 자세히 설명하겠습니다. 이 글을 통해 독자들은 FastAPI로 안전한 애플리케이션을 개발하는 데 필요한 지식을 습득할 수 있을 것입니다.

1. FastAPI 소개

FastAPI는 Python으로 작성된 최신 웹 프레임워크로, 매우 빠르고 현대적인 API를 개발하기 위해 설계되었습니다. FastAPI는 다음과 같은 주요 특징을 가지고 있습니다:

  • 높은 성능: FastAPI는 Starlette를 기반으로 하여 비동기 처리를 최적화하고, Uvicorn 등의 비동기 서버와 함께 사용할 수 있습니다.
  • 자동 문서화: FastAPI는 OpenAPI 및 JSON Schema를 지원하여 문서화를 자동으로 생성할 수 있습니다.
  • 쉬운 사용: 간단한 코드로 복잡한 API를 구축할 수 있습니다.

1.1 FastAPI 설치

FastAPI를 사용하려면 먼저 FastAPI와 Uvicorn을 설치해야 합니다. 다음 명령어를 사용하여 설치할 수 있습니다:

pip install fastapi uvicorn

2. OAuth2와 JWT 개요

OAuth2는 사용자가 제3자 애플리케이션에 대한 접근을 제어할 수 있도록 해주는 권한 부여 프레임워크입니다. JWT는 인증 정보를 안전하게 전송하기 위해 정보의 전자 서명된 JSON 객체입니다. 이 두 기술을 결합하면 안전하고 신뢰할 수 있는 인증 시스템을 구축할 수 있습니다.

2.1 OAuth2 흐름

OAuth2는 다음과 같은 흐름을 따릅니다:

  1. 사용자가 클라이언트 애플리케이션에서 로그인 요청을 합니다.
  2. 클라이언트 애플리케이션은 인증 서버에 사용자 인증을 요청합니다.
  3. 인증 서버는 사용자 자격 증명을 검증한 후, 클라이언트에 액세스 토큰과 갱신 토큰을 발급합니다.
  4. 클라이언트는 API 요청 시 이 액세스 토큰을 사용하여 보호된 리소스에 접근합니다.

2.2 JWT 구조

JWT는 세 부분으로 구성됩니다:

  • Header: 토큰의 타입과 서명 알고리즘 정보를 포함합니다.
  • Payload: 사용자 정보와 같은 클레임(Claims)를 포함합니다.
  • Signature: Header와 Payload를 인코딩하여 비밀 키로 서명한 값입니다.

3. FastAPI에서 OAuth2와 JWT 구현하기

이제 FastAPI를 사용하여 OAuth2와 JWT를 통한 인증 시스템을 구현해 보겠습니다. 이를 위해 기본적인 FastAPI 애플리케이션을 설정하고, OAuth2와 JWT 로직을 추가하겠습니다.

3.1 FastAPI 애플리케이션 기본 설정


from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"Hello": "World"}

위 코드는 FastAPI 애플리케이션의 기본 구조입니다. 이제 이 앱에 OAuth2와 JWT를 구현할 것입니다.

3.2 사용자 모델 및 데이터베이스 설정


from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel
from passlib.context import CryptContext
from typing import Optional
from datetime import datetime, timedelta
import jwt

# 사용자 데이터베이스 시뮬레이션
fake_users_db = {
    "user@example.com": {
        "username": "johndoe",
        "full_name": "John Doe",
        "email": "user@example.com",
        "hashed_password": "$2b$12$KIXyhuOfCJcZawGBY5frXOCeC7g0BU6oq/6kyPzA/9eT4M1Og1Wiy",  # 비밀번호: secret
        "disabled": False,
    }
}

# 유효성 검사 및 해싱 암호화
class User(BaseModel):
    username: str
    full_name: Optional[str] = None
    email: str
    disabled: Optional[bool] = None

class UserInDB(User):
    hashed_password: str

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

def verify_password(plain_password, hashed_password):
    return pwd_context.verify(plain_password, hashed_password)

def get_user(db, email: str):
    if email in db:
        user_data = db[email]
        return UserInDB(**user_data)

# OAuth2PasswordBearer 설정
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

3.3 사용자 인증


def authenticate_user(db, email: str, password: str):
    user = get_user(db, email)
    if not user or not verify_password(password, user.hashed_password):
        return False
    return user

# JWT 생성
SECRET_KEY = "your_secret_key"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

3.4 토큰 경로 추가


@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
    user = authenticate_user(fake_users_db, form_data.username, form_data.password)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="잘못된 사용자 이름 또는 비밀번호",
            headers={"WWW-Authenticate": "Bearer"},
        )
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user.email}, expires_delta=access_token_expires
    )
    return {"access_token": access_token, "token_type": "bearer"}

3.5 보호된 경로 추가


@app.get("/users/me")
async def read_users_me(token: str = Depends(oauth2_scheme)):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="유효하지 않은 토큰",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        email: str = payload.get("sub")
        if email is None:
            raise credentials_exception
    except jwt.PyJWTError:
        raise credentials_exception
    user = get_user(fake_users_db, email)
    if user is None:
        raise credentials_exception
    return user

3.6 애플리케이션 실행

모든 설정이 완료되었으면 FastAPI 서버를 실행할 수 있습니다. 아래 명령어를 사용하여 서버를 실행하십시오:

uvicorn main:app --reload

이제 브라우저를 통해 http://127.0.0.1:8000/docs에 접속하여 API 명세를 확인하고, Swagger UI를 통해 API를 테스트할 수 있습니다.

4. 결론

FastAPI와 함께 OAuth2 및 JWT를 사용하여 애플리케이션의 인증 및 권한 부여 시스템을 구현하는 방법을 살펴보았습니다. 이 강좌를 통해 기본적인 사용자 인증 로직을 이해하고 FastAPI에서 어떻게 구현할 수 있는지를 배울 수 있었습니다. 실제 환경에서는 보다 복잡한 요구 사항이 있을 수 있으므로, 추가적인 보안 조치(예: SSL/TLS 암호화, 비밀 키 관리 등)를 고려하는 것이 중요합니다. FastAPI를 사용하여 더 안전하고 강력한 백엔드 애플리케이션을 구축하시길 바랍니다.