본문으로 건너뛰기
김신건의 로그

[Django] DRF Permission, Throttle, Filter

· 수정 · 📖 약 1분 · 376자/단어 #django #drf #permission #throttle #security
drf permission, drf throttling, drf filter, django object permission, drf auth

정의

DRF는 view 호출 전 인증(Authentication) → 권한(Permission) → 스로틀(Throttle) 순으로 검사. 각 단계가 통과해야 view 메서드 실행. 모두 클래스 기반이라 조합·재사용 가능.

빌트인 Permission

from rest_framework import permissions

# settings.py
REST_FRAMEWORK = {
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.IsAuthenticated",
    ],
}
Permission동작
AllowAny누구나
IsAuthenticated로그인된 사용자
IsAdminUseris_staff=True
IsAuthenticatedOrReadOnly인증 또는 SAFE_METHODS (GET/HEAD/OPTIONS)
DjangoModelPermissionsDjango model permission 활용
DjangoObjectPermissionsobject-level (django-guardian 등)
class PostViewSet(ModelViewSet):
    permission_classes = [IsAuthenticatedOrReadOnly]

커스텀 Permission

from rest_framework import permissions

class IsAuthorOrReadOnly(permissions.BasePermission):
    """글 작성자만 수정/삭제. 다른 사용자는 읽기만."""

    def has_permission(self, request, view):
        return request.method in permissions.SAFE_METHODS or request.user.is_authenticated

    def has_object_permission(self, request, view, obj):
        if request.method in permissions.SAFE_METHODS:
            return True
        return obj.author == request.user
  • has_permission: list/create 등 view 레벨
  • has_object_permission: retrieve/update/destroy 등 객체 레벨 (get_object() 호출 시 자동)
class PostViewSet(ModelViewSet):
    permission_classes = [IsAuthenticated, IsAuthorOrReadOnly]

액션별 permission

class PostViewSet(ModelViewSet):

    def get_permissions(self):
        if self.action == "destroy":
            return [IsAdminUser()]
        elif self.action in ["update", "partial_update"]:
            return [IsAuthorOrReadOnly()]
        elif self.action == "list":
            return [AllowAny()]
        return [IsAuthenticated()]

Object-level (django-guardian)

pip install django-guardian
INSTALLED_APPS += ["guardian"]
AUTHENTICATION_BACKENDS = [
    "django.contrib.auth.backends.ModelBackend",
    "guardian.backends.ObjectPermissionBackend",
]
from guardian.shortcuts import assign_perm

assign_perm("view_post", user, post)
user.has_perm("view_post", post)
from rest_framework.permissions import DjangoObjectPermissions

class PostViewSet(ModelViewSet):
    permission_classes = [DjangoObjectPermissions]

queryset도 자동 필터:

from guardian.shortcuts import get_objects_for_user

def get_queryset(self):
    return get_objects_for_user(self.request.user, "blog.view_post")

Throttling

REST_FRAMEWORK = {
    "DEFAULT_THROTTLE_CLASSES": [
        "rest_framework.throttling.AnonRateThrottle",
        "rest_framework.throttling.UserRateThrottle",
    ],
    "DEFAULT_THROTTLE_RATES": {
        "anon": "100/day",
        "user": "1000/day",
        "burst": "60/min",
        "sustained": "1000/day",
    }
}

단위: /second, /minute, /hour, /day.

커스텀 throttle

from rest_framework.throttling import UserRateThrottle, AnonRateThrottle

class BurstRateThrottle(UserRateThrottle):
    scope = "burst"

class SustainedRateThrottle(UserRateThrottle):
    scope = "sustained"

class PostViewSet(ModelViewSet):
    throttle_classes = [BurstRateThrottle, SustainedRateThrottle]

Scoped throttle

class ContactThrottle(ScopedRateThrottle):
    pass

REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"] = {
    "contact": "5/hour",
}

class ContactViewSet(ViewSet):
    throttle_scope = "contact"
    throttle_classes = [ScopedRateThrottle]

특정 엔드포인트만.

Redis 백엔드

기본은 Django cache. Redis 권장 (분산):

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": env("REDIS_URL"),
    },
}

throttle도 자동으로 Redis 사용.

Filtering

REST_FRAMEWORK = {
    "DEFAULT_FILTER_BACKENDS": [
        "django_filters.rest_framework.DjangoFilterBackend",
        "rest_framework.filters.SearchFilter",
        "rest_framework.filters.OrderingFilter",
    ]
}
pip install django-filter

FilterSet

import django_filters

class PostFilter(django_filters.FilterSet):
    min_views = django_filters.NumberFilter(field_name="views", lookup_expr="gte")
    max_views = django_filters.NumberFilter(field_name="views", lookup_expr="lte")
    title = django_filters.CharFilter(lookup_expr="icontains")
    created_after = django_filters.DateFilter(field_name="created_at", lookup_expr="gte")
    is_published = django_filters.BooleanFilter()
    tags = django_filters.ModelMultipleChoiceFilter(
        field_name="tags__name",
        to_field_name="name",
        queryset=Tag.objects.all(),
    )

    class Meta:
        model = Post
        fields = ["author", "is_published"]

class PostViewSet(ModelViewSet):
    filterset_class = PostFilter

쿼리:

GET /api/posts/?title=django&min_views=100&tags=python&tags=web

Search / Ordering

class PostViewSet(ModelViewSet):
    search_fields = ["title", "body", "author__username"]
    ordering_fields = ["created_at", "views", "title"]
    ordering = ["-created_at"]    # 기본
GET /api/posts/?search=django
GET /api/posts/?ordering=-views
GET /api/posts/?ordering=-views,title

search_fields 접두:

  • ^: starts with
  • =: 정확
  • @: full-text (PG만)
  • $: regex

커스텀 filter backend

from rest_framework import filters

class IsOwnerFilterBackend(filters.BaseFilterBackend):
    """현재 사용자의 객체만 노출."""

    def filter_queryset(self, request, queryset, view):
        return queryset.filter(owner=request.user)

class PostViewSet(ModelViewSet):
    filter_backends = [IsOwnerFilterBackend]

자주 보는 패턴

Token 만료

from rest_framework.throttling import UserRateThrottle

class TokenRefreshThrottle(UserRateThrottle):
    scope = "token_refresh"
    rate = "5/hour"

REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["token_refresh"] = "5/hour"

비밀번호 시도, 토큰 재발급 등 민감 endpoint에 강한 throttle.

Permission + 객체 + 필드

class PostSerializer(ModelSerializer):
    class Meta:
        model = Post
        fields = ["id", "title", "body", "author", "internal_note"]

    def to_representation(self, instance):
        rep = super().to_representation(instance)
        if not self.context["request"].user.is_staff:
            rep.pop("internal_note", None)
        return rep

권한별 다른 필드 노출.

Multi-tenant

class TenantFilterBackend(filters.BaseFilterBackend):
    def filter_queryset(self, request, queryset, view):
        return queryset.filter(tenant=request.tenant)

class PostViewSet(ModelViewSet):
    filter_backends = [TenantFilterBackend]
    permission_classes = [IsAuthenticated, IsTenantMember]

ProblemDetail 응답

class CustomThrottleException(Throttled):
    default_detail = "Rate limit exceeded"
    default_code = "throttled"
# 또는 예외 핸들러
from rest_framework.views import exception_handler

def custom_exception_handler(exc, context):
    response = exception_handler(exc, context)
    if isinstance(exc, Throttled):
        response.data = {
            "error": "Rate limit exceeded",
            "wait_seconds": exc.wait,
        }
    return response

함정

1. permission_classes vs get_permissions

클래스 변수는 모든 액션 공통. action별 다르면 get_permissions() 메서드.

2. has_object_permission는 list에 호출 X

def has_object_permission(self, request, view, obj):
    return obj.owner == request.user

# list 액션은 호출 안 됨 → 필터링 안 됨

list에서 객체 필터는 get_queryset()에서.

3. throttle rate 변경

settings 변경 후 캐시 비워야 새 rate 적용.

4. SAFE_METHODS

permissions.SAFE_METHODS = ["GET", "HEAD", "OPTIONS"]

이걸 비교. PUT/PATCH/DELETE는 unsafe.

5. JWT 만료 + throttle

JWT_AUTH_RETURN_EXPIRATION = True

만료된 토큰으로 요청 → 401, 그러나 throttle 카운트 증가 가능. 401은 throttle 제외 로직 추가 검토.

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기