Django REST Framework: 인증, 권한, 요청 제한 구현

시작하기: 애플리케이션 및 관리자 설정

Django REST Framework (DRF)의 인증, 권한, 요청 제한 기능을 학습하기 위해 먼저 새로운 Django 서브 애플리케이션을 생성하고 관리자 계정을 설정하겠습니다.

python manage.py startapp opt_api

이후 로그인 기능이 필요하므로 Django 내장 관리자 사이트를 활용하고 슈퍼유저를 생성합니다.

python3 manage.py makemigrations
python3 manage.py migrate
python3 manage.py createsuperuser

관리자 계정 생성 후, settings.py에서 언어 및 시간대 설정을 조정할 수 있습니다. 예를 들어, 한국어로 설정하려면 다음과 같습니다.

# settings.py
LANGUAGE_CODE = 'ko-kr' # 한국어
TIME_ZONE = 'Asia/Seoul' # 시간대 설정
USE_I18N = True # 국제화 사용 여부
USE_L10N = True # 지역화 사용 여부
USE_TZ = True # 데이터베이스에서 TIME_ZONE 사용 여부 (True: 지정된 시간대, False: UTC)

1. 인증(Authentication)

인증은 클라이언트가 누구인지 식별하는 과정입니다. DRF는 다양한 인증 방식을 지원하며, 직접 정의하거나 내장된 방식을 사용할 수 있습니다.

1.1 사용자 정의 인증 방식

1.1.1 모델 정의

사용자 정보와 토큰을 저장할 모델을 정의합니다. 다음은 예시입니다.

# opt_api/models.py
from django.db import models

class SystemUser(models.Model):
    user_identifier = models.CharField(max_length=32, unique=True)
    hashed_password = models.CharField(max_length=32)
    access_level = models.IntegerField(choices=((1, '관리자'), (2, '일반 사용자'), (3, '게스트')))

    def __str__(self):
        return self.user_identifier

class AccessKey(models.Model):
    associated_user = models.OneToOneField(to='SystemUser', on_delete=models.CASCADE)
    secret_key = models.CharField(max_length=64)

    def __str__(self):
        return f"{self.associated_user.user_identifier}'s key"

1.1.2 사용자 정의 인증 클래스 구현

DRF의 BaseAuthentication을 상속받아 사용자 정의 인증 클래스를 만듭니다. 이 클래스는 요청에서 토큰을 추출하고 유효성을 검사합니다.

# opt_api/auth_backends.py
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from . import models

class CustomTokenAuthentication(BaseAuthentication):
    def authenticate(self, request):
        # 요청 헤더 또는 쿼리 파라미터에서 토큰을 추출합니다.
        # 여기서는 쿼리 파라미터 'auth_token'을 사용합니다.
        token_value = request.GET.get('auth_token')
        
        if not token_value:
            return None # 토큰이 없으면 다음 인증 백엔드로 넘어갑니다.

        try:
            token_obj = models.AccessKey.objects.select_related('associated_user').get(secret_key=token_value)
            return (token_obj.associated_user, token_obj) # (user, auth_object) 반환
        except models.AccessKey.DoesNotExist:
            raise AuthenticationFailed('인증 정보가 유효하지 않습니다.')

    def authenticate_header(self, request):
        # 인증 실패 시 응답 헤더에 추가될 WWW-Authenticate 값을 반환합니다.
        return 'Token'

1.1.3 뷰 작성: 로그인 및 보호된 리소스

사용자 로그인 및 토큰 발급, 그리고 토큰으로 보호되는 리소스에 접근하는 뷰를 구현합니다.

# opt_api/views.py
import hashlib
import time
from rest_framework.views import APIView
from rest_framework.response import Response
from django.http import HttpResponse
from . import models
from .auth_backends import CustomTokenAuthentication # 사용자 정의 인증 클래스 임포트

def generate_secure_token(username_seed):
    """
    사용자 이름을 기반으로 고유한 토큰을 생성합니다.
    실제 환경에서는 더 강력한 토큰 생성 로직을 사용해야 합니다.
    """
    hasher = hashlib.sha256()
    hasher.update(bytes(str(time.time()), encoding='utf-8'))
    hasher.update(bytes(username_seed, encoding='utf-8'))
    return hasher.hexdigest()

class UserLoginView(APIView):
    authentication_classes = [] # 로그인 뷰는 인증을 필요로 하지 않습니다.

    def post(self, request):
        response_data = {'status_code': 1001, 'message': None}
        try:
            input_username = request.data.get('username')
            input_password = request.data.get('password')

            user_account = models.SystemUser.objects.filter(
                user_identifier=input_username, 
                hashed_password=input_password
            ).first()

            if user_account:
                new_token = generate_secure_token(input_username)
                models.AccessKey.objects.update_or_create(
                    associated_user=user_account,
                    defaults={'secret_key': new_token}
                )
                response_data['status_code'] = 1000
                response_data['message'] = '로그인 성공'
                response_data['access_token'] = new_token
            else:
                response_data['message'] = '사용자 이름 또는 비밀번호가 잘못되었습니다.'
        except Exception as e:
            response_data['message'] = str(e)
        return Response(response_data)

class ProtectedResourceView(APIView):
    # 이 뷰는 CustomTokenAuthentication을 사용하여 인증됩니다.
    authentication_classes = [CustomTokenAuthentication] 

    def get(self, request):
        return HttpResponse(f'인증된 사용자 {request.user.user_identifier}님, 환영합니다! (GET)')

    def post(self, request):
        return HttpResponse(f'인증된 사용자 {request.user.user_identifier}님, 데이터를 처리했습니다. (POST)')

1.1.4 전역 인증 설정

settings.pyDEFAULT_AUTHENTICATION_CLASSES를 설정하여 모든 뷰에 적용할 수 있습니다.

# settings.py
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": ["opt_api.auth_backends.CustomTokenAuthentication",]
}

1.1.5 개별 뷰 인증 설정

특정 뷰에만 인증 클래스를 적용하려면, 해당 뷰 클래스 내에 authentication_classes 속성을 정의합니다.

# opt_api/views.py
class SomeOtherProtectedView(APIView):
    authentication_classes = [CustomTokenAuthentication]
    # ...

1.2 내장 인증 방식

DRF는 세션 인증 및 기본 인증과 같은 내장 인증 방식을 제공합니다. 이들은 주로 Django의 기본 사용자 모델 및 세션 시스템과 연동됩니다.

# settings.py (전역 설정 예시)
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.SessionAuthentication', # 세션 기반 인증
        'rest_framework.authentication.BasicAuthentication',   # HTTP 기본 인증
    )
}

개별 뷰에 적용하는 방법은 사용자 정의 방식과 동일합니다.

# opt_api/views.py (개별 뷰 설정 예시)
from rest_framework.authentication import SessionAuthentication, BasicAuthentication
from rest_framework.views import APIView

class AdminOnlyView(APIView):
    authentication_classes = [SessionAuthentication, BasicAuthentication]
    # ...

인증 실패 시 DRF는 다음과 같은 응답을 반환할 수 있습니다.

  • 401 Unauthorized: 인증 자격 증명이 누락되었거나 유효하지 않음.
  • 403 Permission Denied: 인증은 되었으나 리소스에 접근할 권한이 없음.

2. 권한(Permissions)

권한은 인증된 사용자가 특정 리소스에 접근하거나 특정 작업을 수행할 수 있는지 결정하는 과정입니다. DRF는 뷰의 dispatch() 메서드 실행 전과 특정 객체 접근 시 권한을 확인합니다.

2.1 사용자 정의 권한

2.1.1 권한 클래스 구현

BasePermission을 상속받아 사용자 정의 권한 클래스를 구현합니다. 다음은 관리자 레벨 사용자만 접근을 허용하는 예시입니다.

# opt_api/permissions.py
from rest_framework.permissions import BasePermission

class AdminLevelAccessPermission(BasePermission):
    message = '이 작업은 관리자만 수행할 수 있습니다.'

    def has_permission(self, request, view):
        # request.user는 인증 과정에서 설정된 사용자 객체입니다.
        # 관리자 레벨 (access_level=1) 사용자만 접근을 허용합니다.
        if request.user and request.user.is_authenticated:
            return request.user.access_level == 1
        return False

2.1.2 전역 권한 설정

settings.pyDEFAULT_PERMISSION_CLASSES를 통해 전역으로 권한 클래스를 설정할 수 있습니다.

# settings.py
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": ["opt_api.auth_backends.CustomTokenAuthentication",],
    "DEFAULT_PERMISSION_CLASSES": ["opt_api.permissions.AdminLevelAccessPermission",]
}

2.1.3 개별 뷰 권한 설정

특정 뷰에만 권한 클래스를 적용하려면 permission_classes 속성을 사용합니다.

# opt_api/views.py
from .permissions import AdminLevelAccessPermission

class AdminDashboardView(APIView):
    authentication_classes = [CustomTokenAuthentication]
    permission_classes = [AdminLevelAccessPermission]

    def get(self, request):
        return HttpResponse(f"관리자님, 대시보드 정보입니다.")

2.1.4 사용자 정의 권한 메서드

BasePermission을 상속받아 다음 두 메서드 중 하나 또는 둘 다 구현할 수 있습니다.

  • .has_permission(self, request, view): 뷰 자체에 대한 접근 권한을 확인합니다.
  • .has_object_permission(self, request, view, obj): 특정 데이터 객체에 대한 접근 권한을 확인합니다 (예: 사용자가 자신이 생성한 게시물만 수정할 수 있도록).

2.2 내장 권한 방식

DRF는 자주 사용되는 몇 가지 내장 권한 클래스를 제공합니다.

2.2.1 내장 권한 클래스 종류

from rest_framework.permissions import AllowAny, IsAuthenticated, IsAdminUser, IsAuthenticatedOrReadOnly

# AllowAny: 모든 사용자에게 접근 허용 (기본값)
# IsAuthenticated: 인증된 사용자에게만 접근 허용
# IsAdminUser: Django is_staff 플래그가 True인 사용자에게만 접근 허용 (관리자)
# IsAuthenticatedOrReadOnly: 인증된 사용자는 모든 작업 허용, 미인증 사용자는 읽기 작업만 허용

2.2.2 전역 권한 설정 예시

# settings.py
REST_FRAMEWORK = {
    # ...
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated', # 모든 API는 인증된 사용자만 접근 가능
    )
}

만약 DEFAULT_PERMISSION_CLASSES가 설정되지 않으면, AllowAny가 기본값으로 적용됩니다.

2.2.3 개별 뷰 권한 설정 예시

# opt_api/views.py
from rest_framework.permissions import IsAuthenticated
from rest_framework.views import APIView

class MyProfileView(APIView):
    permission_classes = (IsAuthenticated,) # 이 뷰는 인증된 사용자만 접근 가능
    # ...

3. 스로틀링(Throttling)

스로틀링은 클라이언트가 API를 호출하는 빈도를 제한하여 서버 과부하를 방지하고 공정한 리소스 사용을 보장하는 기능입니다. 주로 유료 서비스, 투표 시스템 등에 사용됩니다.

3.1 사용자 정의 요청 제한 클래스

3.1.1 요청 제한 클래스 구현

SimpleRateThrottle을 상속받아 사용자 정의 스로틀링 클래스를 구현할 수 있습니다. 다음은 IP 주소를 기반으로 1분당 3회 요청을 허용하는 예시입니다.

# opt_api/throttling.py
import time
from rest_framework.throttling import BaseThrottle

class IPCustomThrottle(BaseThrottle):
    _visit_records = {} # 클래스 변수로 IP별 방문 기록 저장
    rate = 3 # 1분당 최대 요청 횟수
    duration = 60 # 제한 시간 (초)

    def allow_request(self, request, view):
        client_ip = request.META.get('REMOTE_ADDR')
        current_time = time.time()

        if client_ip not in self._visit_records:
            self._visit_records[client_ip] = [current_time]
            return True

        # 오래된 기록 제거: duration보다 오래된 요청 기록은 삭제
        self._visit_records[client_ip] = [
            t for t in self._visit_records[client_ip] if current_time - t < self.duration
        ]

        # 요청 횟수 확인
        if len(self._visit_records[client_ip]) < self.rate:
            self._visit_records[client_ip].insert(0, current_time) # 최신 요청 시간을 목록의 맨 앞에 추가
            return True
        else:
            return False

    def wait(self):
        # 다음 요청까지 기다려야 하는 시간을 계산합니다.
        # 가장 오래된 요청 시간 (즉, 리스트의 마지막 요소)을 기준으로 계산합니다.
        client_ip = self.request.META.get('REMOTE_ADDR')
        if client_ip in self._visit_records and self._visit_records[client_ip]:
            oldest_request_time = self._visit_records[client_ip][-1]
            return self.duration - (time.time() - oldest_request_time)
        return None # 오류 상황

3.1.2 전역 요청 제한 설정

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [
        'opt_api.throttling.IPCustomThrottle',
    ],
}

3.1.3 개별 뷰 요청 제한 설정

# opt_api/views.py
from .throttling import IPCustomThrottle

class LimitedAccessView(APIView):
    throttle_classes = [IPCustomThrottle] # 이 뷰에만 요청 제한 적용

    def get(self, request):
        return HttpResponse('제한된 접근 뷰입니다.')

3.2 내장 요청 제한 클래스

DRF는 익명 사용자, 인증된 사용자, 그리고 스코프별로 요청을 제한하는 내장 클래스를 제공합니다.

3.2.1 IP 기반 요청 제한 (SimpleRateThrottle 상속)

# opt_api/throttling.py
from rest_framework.throttling import SimpleRateThrottle

class ClientIPRateThrottle(SimpleRateThrottle):
    # 'api_general_limit' 스코프에 연결됩니다.
    # settings.py의 DEFAULT_THROTTLE_RATES에서 이 스코프의 제한 속도를 정의해야 합니다.
    scope = 'api_general_limit'

    def get_cache_key(self, request, view):
        # 클라이언트의 IP 주소를 캐시 키로 사용합니다.
        return self.get_ident(request)

settings.py에 제한 속도를 정의합니다.

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_RATES': {
        'api_general_limit': '5/minute' # 'api_general_limit' 스코프는 분당 5회 요청 허용
    }
}

3.2.2 익명 사용자 요청 제한

IP 주소를 사용하여 익명(미인증) 사용자의 요청을 제한합니다.

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': (
        'rest_framework.throttling.AnonRateThrottle',
    ),
    'DEFAULT_THROTTLE_RATES': {
        'anon': '3/minute', # 익명 사용자는 분당 3회 요청 허용
    }
}
# 'second', 'minute', 'hour', 'day'를 사용하여 주기를 지정할 수 있습니다.

3.2.3 인증된 사용자 요청 제한

사용자 ID를 사용하여 인증된 사용자의 요청을 제한합니다.

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': (
        'rest_framework.throttling.UserRateThrottle',
    ),
    'DEFAULT_THROTTLE_RATES': {
        'user': '10/minute' # 인증된 사용자는 분당 10회 요청 허용
    }
}

3.2.4 스코프 기반 요청 제한 (ScopedRateThrottle)

특정 뷰 그룹에 대해 서로 다른 요청 제한을 적용할 때 유용합니다.

# opt_api/views.py
from rest_framework.views import APIView
from rest_framework.response import Response

class ContactListAPI(APIView):
    throttle_scope = 'contact_access' # 'contact_access' 스코프 사용
    def get(self, request):
        return Response({"message": "연락처 목록입니다."})

class FileUploadAPI(APIView):
    throttle_scope = 'file_uploads' # 'file_uploads' 스코프 사용
    def post(self, request):
        return Response({"message": "파일이 업로드되었습니다."})

settings.py에서 각 스코프의 제한 속도를 정의합니다.

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': (
        'rest_framework.throttling.ScopedRateThrottle',
    ),
    'DEFAULT_THROTTLE_RATES': {
        'contact_access': '100/day', # 연락처 접근은 하루에 100회
        'file_uploads': '5/hour'    # 파일 업로드는 시간당 5회
    }
}

3.2.5 요청 제한 에러 메시지 사용자 정의

스로틀링으로 인해 요청이 제한될 때 반환되는 에러 메시지를 사용자 정의할 수 있습니다.

# opt_api/views.py
from rest_framework.exceptions import Throttled
from django.http import HttpResponse

class CustomThrottledView(APIView):
    authentication_classes = [CustomTokenAuthentication]
    # permission_classes = [AdminLevelAccessPermission] # 필요에 따라 추가
    throttle_classes = [IPCustomThrottle] # 사용자 정의 스로틀링 적용

    def get(self, request):
        return HttpResponse('테스트 응답입니다.')

    def throttled(self, request, wait):
        # Throttled 예외를 상속받아 메시지를 커스터마이징합니다.
        class KoreanThrottled(Throttled):
            default_detail = '요청 횟수 제한 초과!'
            extra_detail_singular = '잠시 후 다시 시도해주세요. {wait}초 후에 접근 가능합니다.'
            extra_detail_plural = '잠시 후 다시 시도해주세요. {wait}초 후에 접근 가능합니다.'
        
        raise KoreanThrottled(wait)

이 기능을 활용하여 API 사용 빈도를 효과적으로 제어하고 사용자에게 친절한 피드백을 제공할 수 있습니다.

태그: DjangoRESTFramework authentication Permissions Throttling APIsecurity

7월 18일 20:19에 게시됨