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 인프라 구축에 더 가까워지게 합니다.