knox와 FastAPI-Users를 사용한 Python API의 토큰 기반 인증 강화
Lukas Schneider
DevOps Engineer · Leapcell

소개
현대 웹 개발 환경에서 API 보안은 가장 중요한 문제로 대두됩니다. 애플리케이션이 점점 더 분산되고 마이크로서비스 아키텍처가 확산됨에 따라, 강력한 인증 메커니즘은 더 이상 사치가 아니라 필수 사항이 되었습니다. 토큰 기반 인증은 상태 비저장 특성, 확장성, 모바일 및 단일 페이지 애플리케이션에 대한 적합성 덕분에 API 보안을 위한 선호되는 방법으로 부상했습니다. 하지만 단순히 토큰을 사용하는 것만으로는 충분하지 않습니다. 토큰의 생명주기 관리, 기밀성 보장, 일반적인 공격 벡터 완화에는 신중한 고려가 필요합니다. 이 글에서는 Django REST Framework를 위한 django-rest-knox와 FastAPI를 위한 FastAPI-Users라는 두 가지 강력한 프레임워크가 토큰 인증을 위한 향상된 보안 기능을 어떻게 제공하는지, 기본 구현을 넘어서는 더욱 복원력 있고 개발자 친화적인 솔루션을 제공하는지에 대해 알아봅니다.
안전한 토큰 인증의 핵심 개념
django-rest-knox와 FastAPI-Users의 구체적인 내용으로 들어가기 전에, 안전한 토큰 인증을 이해하는 데 중요한 몇 가지 기본 개념을 명확히 해보겠습니다.
토큰 기반 인증
핵심적으로 토큰 기반 인증은 클라이언트가 인증 서버에 자격 증명(사용자 이름 및 비밀번호 등)을 보내는 것을 포함합니다. 성공적인 검증 후 서버는 암호화된 토큰을 발급합니다. 이 토큰은 일반적으로 JSON Web Token(JWT) 또는 불투명 토큰이며, 이후 보호된 리소스에 액세스하기 위한 모든 요청과 함께 클라이언트 측에 저장되어 전송됩니다. 서버는 각 요청에 대해 사용자를 인증하기 위해 토큰을 검증합니다.
불투명 토큰 vs. JWT
- 불투명 토큰: 서버 측에 저장된 인증 세션에 대한 참조로 작동하는 임의의 문자열입니다. 토큰 자체에는 사용자 정보가 포함되어 있지 않습니다. 서버는 세션 세부 정보를 검색하기 위해 데이터베이스에서 토큰을 조회합니다. 이를 통해 서버 측 취소 및 쉬운 무효화가 가능합니다.
- JWT (JSON Web Tokens): 사용자 클레임(사용자에 대한 정보)과 서명을 포함하는 인코딩된 JSON 개체를 포함하는 토큰입니다. 서버는 서명 키만 있으면 매번 데이터베이스를 쿼할 필요 없이 토큰의 무결성을 확인할 수 있습니다. 효율적이지만, JWT는 만료 시간이 길면 즉시 취소하기가 더 어렵습니다.
토큰과 관련된 주요 보안 문제
- 무차별 대입 공격: 토큰 또는 자격 증명을 추측하려는 반복적인 시도.
- 재전송 공격: 공격자가 토큰을 가로채서 무단 액세스를 위해 재사용합니다.
- 토큰 가로채기/도난: 공격자가 종종 크로스 사이트 스크립팅(XSS) 또는 중간자 공격을 통해 유효한 토큰을 획득합니다.
- 부적절한 토큰 취소: 사용자가 로그아웃하거나 세션이 종료되어야 한 후에도 토큰이 유효하게 유지됩니다.
- 안전하지 않은 저장: 토큰이 클라이언트 측의 취약한 위치에 저장됩니다.
논의된 프레임워크는 디자인과 기능을 통해 이러한 문제를 해결하는 것을 목표로 합니다.
django-rest-knox를 통한 보안 강화
django-rest-knox는 Django REST Framework(DRF)를 위한 패키지로, 안전하고 토큰 기반인 인증 시스템을 제공합니다. DRF의 내장 TokenAuthentication은 단일의 지속적인 토큰을 발급하는 반면, knox는 단일 사용, 만료, 쉽게 취소 가능한 토큰에 중점을 둡니다. 주로 불투명 토큰을 사용합니다.
django-rest-knox 작동 방식
- 토큰 생성: 사용자가 로그인하면 knox는 고유하고 암호학적으로 안전한 불투명 토큰을 생성합니다. 데이터베이스에 이 토큰의 해시와 만료 날짜를 저장합니다.
- 클라이언트 측: 일반 텍스트 토큰이 클라이언트로 전송됩니다. 클라이언트는 이 토큰을 저장합니다(예: 웹 앱의
localStorage또는sessionStorage, 모바일 앱의 보안 저장소). - 인증: 후속 요청에 대해 클라이언트는 이 토큰을
Authorization헤더에 전송합니다. - 검증: 서버는 토큰을 수신하고, 해시하고, 데이터베이스에 저장된 해시와 비교합니다. 일치하는 항목이 발견되고 토큰이 만료되지 않았다면 사용자가 인증됩니다.
- 단일 사용/취소: KNOX는 로그아웃 시 또는 만료 시 토큰 취소를 허용합니다. 중요하게도, 사용자당 여러 토큰을 허용하여 다중 장치 로그인을 지원하며, 사용자 또는 특정 토큰에 대한 모든 토큰을 취소하는 메커니즘을 제공합니다.
django-rest-knox 구현 예시
먼저 django-rest-knox를 설치합니다:
pip install django-rest-knox
settings.py에서 knox를 INSTALLED_APPS에 추가합니다:
# settings.py INSTALLED_APPS = [ # ...다른 앱 'rest_framework', 'knox', ] REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'knox.auth.TokenAuthentication', ), # ... }
프로젝트의 urls.py에 Knox의 URL을 포함시킵니다:
# urls.py from django.contrib import admin from django.urls import path, include from knox import views as knox_views from .views import LoginAPI # 사용자 지정 LoginAPI 뷰가 있다고 가정 urlpatterns = [ path('admin/', admin.site.urls), path('api/auth/login/', LoginAPI.as_view(), name='knox_login'), path('api/auth/logout/', knox_views.LogoutView.as_view(), name='knox_logout'), path('api/auth/logoutall/', knox_views.LogoutAllView.as_view(), name='knox_logoutall'), # ...다른 API 엔드포인트 ]
사용자 로그인 및 토큰 생성을 처리하는 사용자 지정 LoginAPI 뷰를 만듭니다:
# your_app/views.py from rest_framework import generics, permissions from rest_framework.response import Response from knox.models import AuthToken from .serializers import UserSerializer, AuthTokenSerializer class LoginAPI(generics.GenericAPIView): serializer_class = AuthTokenSerializer permission_classes = (permissions.AllowAny,) def post(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) user = serializer.validated_data['user'] _, token = AuthToken.objects.create(user) # 토큰을 생성하고 사용자 객체와 토큰 문자열을 반환 return Response({ "user": UserSerializer(user, context=self.get_serializer_context()).data, "token": token }) # your_app/serializers.py from rest_framework import serializers from django.contrib.auth.models import User # 또는 사용자 지정 사용자 모델 from django.contrib.auth import authenticate class UserSerializer(serializers.ModelSerializer): class Meta: model = User fields = ('id', 'username', 'email') class AuthTokenSerializer(serializers.Serializer): username = serializers.CharField() password = serializers.CharField() def validate(self, data): user = authenticate(**data) if user and user.is_active: return {'user': user} raise serializers.ValidationError("잘못된 자격 증명")
이 설정으로 성공적인 로그인은 새로운 토큰을 반환합니다. 사용자가 /api/auth/logout/을 통해 로그아웃하면 해당 토큰이 취소됩니다. /api/auth/logoutall/을 사용하면 해당 사용자에게 유효한 모든 토큰이 취소되어 강력한 보안 기능을 제공합니다. 예를 들어, 사용자가 계정 침해를 의심하는 경우입니다.
FastAPI-Users를 통한 보안 강화
FastAPI-Users는 FastAPI 애플리케이션에서 사용자 관리 및 인증을 위한 포괄적이고 유연한 솔루션을 제공합니다. 본질적으로 JWT를 지원하고, 강력한 사용자 등록, 비밀번호 재설정, OAuth2 및 세션 인증과 같은 구성 가능한 보안 기능을 제공합니다. 강점은 모듈성과 FastAPI의 종속성 주입 시스템과의 훌륭한 통합에 있습니다.
FastAPI-Users 작동 방식
FastAPI-Users는 FastAPI의 APIRouter와 통합되어 인증, 등록, 비밀번호 재설정 및 사용자 관리를 위한 준비된 엔드포인트를 제공합니다. JWT가 일반적이고 안전한 선택지로 다양한 인증 백엔드를 지원합니다.
- 사용자 모델: 사용자 정보(해시된 비밀번호 포함)를 저장하기 위해 사용자 지정 사용자 모델(종종
SQLAlchemyUserDatabase또는 유사한 통합 기반)이 필요합니다. - 인증 백엔드 (예: JWT): 성공적인 로그인 후 액세스 토큰(JWT)과 선택적으로 새로 고침 토큰이 생성됩니다. 액세스 토큰에는 인코딩된 사용자 정보와 서명이 포함되어 상태 비저장 검증을 허용합니다.
- 종속성 주입: FastAPI-Users는 FastAPI의 종속성 주입을 활용하여 라우트 핸들러에
current_user개체를 제공하여 액세스 제어를 단순화합니다. - 보안 기능: 비밀번호 해싱(
passlib사용), JWT에 대한 안전한 쿠키 처리, 이메일 확인 및 토큰을 사용한 비밀번호 재설정 흐름을 위한 기본 지원이 포함되어 있습니다.
FastAPI-Users 구현 예시
먼저 fastapi-users 및 종속성(예: fastapi, uvicorn, sqlalchemy, PostgreSQL용 asyncpg, 해싱용 passlib)을 설치합니다:
pip install fastapi fastapi-users 'uvicorn[standard]' sqlalchemy asyncpg passlib[bcrypt] python-multipart
사용자 모델 및 데이터베이스 어댑터(예: SQLAlchemy)를 정의합니다:
# models.py from typing import Optional from sqlalchemy import Column, String, Boolean from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() class User(Base): __tablename__ = "users" id: str = Column(String, primary_key=True, index=True) # 일반적으로 UUID email: str = Column(String, unique=True, index=True, nullable=False) hashed_password: str = Column(String, nullable=False) is_active: bool = Column(Boolean, default=True, nullable=False) is_superuser: bool = Column(Boolean, default=False, nullable=False) is_verified: bool = Column(Boolean, default=False, nullable=False)
데이터베이스, 인증 백엔드 및 FastAPIUsers 인스턴스를 설정합니다:
# main.py import uuid from typing import AsyncGenerator from fastapi import FastAPI, Depends from fastapi_users import FastAPIUsers, schemas from fastapi_users.authentication import ( AuthenticationBackend, BearerTransport, JWTStrategy, ) from fastapi_users.db import SQLAlchemyUserDatabase from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine from sqlalchemy.orm import sessionmaker from models import Base, User # 사용자 models.py # 데이터베이스 설정 DATABASE_URL = "postgresql+asyncpg://user:password@host:port/dbname" engine = create_async_engine(DATABASE_URL) async_session_maker = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) async def create_db_and_tables(): async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) async def get_async_session() -> AsyncGenerator[AsyncSession, None]: async with async_session_maker() as session: yield session async def get_user_db(session: AsyncSession = Depends(get_async_session)): yield SQLAlchemyUserDatabase(session, User) # 인증 백엔드 (이 경우 JWT) SECRET = "YOUR_SUPER_SECRET_KEY" # 중요: 환경 변수에서 강력하고 무작위적인 키를 사용하세요! bearer_transport = BearerTransport(tokenUrl="auth/jwt/login") def get_jwt_strategy() -> JWTStrategy: return JWTStrategy(secret=SECRET, lifetime_seconds=3600) # 토큰 유효 시간 1시간 auth_backend = AuthenticationBackend( name="jwt", transport=bearer_transport, get_strategy=get_jwt_strategy, ) # FastAPIUsers 인스턴스 fastapi_users = FastAPIUsers[User, uuid.UUID]( get_user_db, [auth_backend], ) # 사용자 스키마 (요청/응답용) class UserRead(schemas.BaseUser[uuid.UUID]): pass class UserCreate(schemas.BaseUserCreate): pass class UserUpdate(schemas.BaseUserUpdate): pass # FastAPI 애플리케이션 app = FastAPI(on_startup=[create_db_and_tables]) # 인증 라우터 추가 app.include_router( fastapi_users.get_auth_router(auth_backend), prefix="/auth/jwt", tags=["auth"], ) app.include_router( fastapi_users.get_register_router(UserRead, UserCreate), prefix="/auth", tags=["auth"], ) app.include_router( fastapi_users.get_reset_password_router(), prefix="/auth", tags=["auth"], ) app.include_router( fastapi_users.get_verify_router(UserRead), prefix="/auth", tags=["auth"], ) app.include_router( fastapi_users.get_users_router(UserRead, UserUpdate), prefix="/users", tags=["users"], ) # 보호된 엔드포인트 예시 current_active_user = fastapi_users.current_user(active=True) @app.get("/authenticated-route") async def authenticated_route(user: User = Depends(current_active_user)): return {"message": f"안녕하세요 {user.email}, 인증되었습니다!"}
이 설정은 JWT를 얻기 위한 /auth/jwt/login, 사용자 생성을 위한 /auth/register 및 비밀번호 재설정 및 이메일 확인을 위한 기타 엔드포인트를 제공합니다. current_active_user 종속성은 JWT 검증을 자동으로 처리하고 인증된 사용자 개체를 라우트 핸들러에 제공합니다. 이를 통해 상당한 상용구 코드를 줄이고 일관되고 안전한 토큰 처리를 보장합니다.
결론
django-rest-knox와 FastAPI-Users는 모두 Python 웹 애플리케이션에 안전한 토큰 기반 인증을 구현하기 위한 강력하고 의견이 있는 솔루션을 제공합니다. django-rest-knox는 Django REST Framework 환경에서 빛을 발하며, 명시적인 세션 관리가 필요한 애플리케이션에 이상적인 강력한 취소 기능을 갖춘 불투명 토큰 생명주기에 대한 세부적인 제어를 제공합니다. 반면에 FastAPI-Users는 FastAPI용 포괄적이고 고도로 구성 가능한 사용자 관리 시스템을 제공하며, JWT를 활용하고 인증 및 사용자 흐름의 복잡성 상당 부분을 추상화합니다. 둘 간의 선택은 프레임워크(Django vs. FastAPI)와 토큰 유형 및 생명주기 관리에 대한 특정 요구 사항에 따라 달라집니다. 궁극적으로 이러한 라이브러리를 채택함으로써 개발자는 API의 보안 상태를 크게 향상시킬 수 있으며, 기본 구현을 넘어 더욱 복원력 있고 유지 관리가 용이한 인증 시스템으로 나아갈 수 있습니다. 이러한 도구를 활용하는 것은 안전하고 확장 가능한 API 인프라 구축에 더 가까워지게 합니다.