Punkt wyjścia: kontekst projektu i wymagania wobec API
Realne use case’y: panel administracyjny, SPA i aplikacja mobilna
Projektując autoryzację, paginację i testy w Django REST Framework, najlepiej myśleć od razu o konkretnych klientach API. Inaczej wygląda API, które obsługuje wyłącznie panel administracyjny dla kilku pracowników, a inaczej takie, które będzie bombardowane tysiącami requestów z aplikacji mobilnej.
Typowe scenariusze:
- Panel administracyjny (wewnętrzny) – zwykle mało użytkowników, zaufane środowisko, dostęp z przeglądarki, logowanie po sesjach Django i formularzu. Tu najczęściej używa się SessionAuthentication i CSRF, a API może być dodatkowo chronione VPN-em.
- Aplikacja SPA (React, Vue, Angular) – klienci przeglądarkowi, często inna domena niż backend, logowanie po tokenach (JWT albo klasyczne tokeny DRF), duży nacisk na precyzyjną autoryzację per zasób i wygodny, przewidywalny mechanizm paginacji.
- Aplikacja mobilna – długotrwałe sesje, rzadkie logowanie, urządzenia mogą być utracone lub skradzione. Tu kluczowe są: bezpieczne przechowywanie tokenów, wygodny mechanizm odświeżania (refresh token), sensowny throttling i wydajna paginacja, która nie zmienia się co deploy.
Ten kontekst wpływa nie tylko na wybór mechanizmu uwierzytelniania, ale też na to, jak dokładnie trzeba zaprojektować paginację i jaki zakres testów jest konieczny, aby nie zaskoczyć produkcji.
Uwierzytelnianie, autoryzacja i throttling – trzy różne sprawy
W rozmowach o bezpieczeństwie API często miesza się trzy pojęcia: uwierzytelnianie (authentication), autoryzacja (authorization, permissions) i throttling. W DRF są to osobne mechanizmy i warto trzymać je mentalnie oddzielnie.
- Uwierzytelnianie – odpowiedź na pytanie „kto to jest?”. DRF rozwiązuje to za pomocą authentication_classes, które ustawiają
request.userirequest.auth. - Autoryzacja – odpowiedź na pytanie „czy ten użytkownik może to zrobić?”. DRF wykorzystuje do tego permission_classes i ewentualnie object-level permissions.
- Throttling – ograniczanie liczby żądań z danego źródła (IP, user, token). Chroni przed nadużyciami i prostymi atakami typu brute-force.
Zbyt częstym błędem jest upychanie całej logiki bezpieczeństwa w permissions i ignorowanie throttlingu, a jednocześnie nieprzemyślane użycie JWT lub tokenów bez ich wygaszania. W efekcie API formalnie „działa”, ale jest trudne do utrzymania i ma dziury na poziomie operacyjnym.
Kluczowe decyzje architektoniczne na starcie
Przed napisaniem pierwszego widoku API dobrze jest odpowiedzieć na kilka pytań, które później trudno zmienić bez bolesnego refaktoru:
- Jaki typ uwierzytelniania? – sesje Django, Basic auth, tokeny DRF, JWT, OAuth2? Dla SPA i mobilki najczęściej kończy się na JWT (np. simplejwt) lub OAuth2 (np. django-oauth-toolkit), dla wewnętrznych paneli – na sesjach.
- Jak szczegółowy model uprawnień? – czy wystarczy podział „user/admin”, czy potrzebne są poziomy takie jak „właściciel obiektu”, „członek projektu”, „gość z dostępem tylko do odczytu”. Od tego zależy użycie custom permissions i object-level permissions.
- Strategia paginacji – liczby stron (PageNumberPagination), limit/offset, czy cursor-based? Decyzja wpływa na kontrakt z frontendem, cache’owanie i stabilność wyników.
- Poziom testów – czy API będzie rozwijane latami przez wiele osób, czy to mały projekt? To determinuję ilość testów integracyjnych, testów bezpieczeństwa, mockowania usług zewnętrznych itp.
Lepszy jest prostszy, ale spójny zestaw decyzji, niż rozproszony miks przypadkowych rozwiązań wprowadzanych „po drodze”.
Przykładowa domena: API do zarządzania zadaniami
Żeby przykłady były spójne, przyjmijmy prostą domenę: system do zarządzania projektami i zadaniami. Mamy modele:
- Project – zespół użytkowników, którzy współpracują nad jednym projektem,
- Task – zadania w ramach projektu, każde należy do projektu, ma właściciela i status.
Założenia bezpieczeństwa:
- Widzieć projekt może tylko osoba, która jest jego członkiem.
- Zadania widzą członkowie projektu, ale edytować może tylko właściciel zadania lub administrator projektu.
- API ma obsługiwać SPA oraz aplikację mobilną – wybór pada na JWT z djangorestframework-simplejwt.
Na tej bazie można omówić autoryzację, paginację list zadań oraz testy endpointów projektów i zadań w Django REST Framework.
Podstawy uwierzytelniania i autoryzacji w Django REST Framework
Mechanizmy authentication_classes i permission_classes
Fundamentem bezpieczeństwa w DRF są dwie listy klas: authentication_classes i permission_classes. Każdy request przechodzi najpierw przez uwierzytelnianie, potem przez autoryzację.
Przykładowa konfiguracja globalna w settings.py:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
}
Dzięki temu wszystkie widoki DRF domyślnie wymagają zalogowania i akceptują dwa typy uwierzytelniania – sesyjne i tokenowe. Kolejność ma znaczenie: DRF stopniowo próbuje kolejne klasy aż znajdzie taką, która zwróci (user, auth) zamiast wyjątku.
Na poziomie widoku można nadpisać te ustawienia:
from rest_framework.views import APIView
from rest_framework.permissions import AllowAny
class PublicPingView(APIView):
permission_classes = [AllowAny]
def get(self, request):
return Response({'status': 'ok'})
Jeśli developerzy nauczą się świadomie ustawiać permission_classes i authentication_classes per widok lub per viewset, znacząco spada ryzyko „przypadkowo” otwartych endpointów.
Domyślne ustawienia a bezpieczeństwo API
Domyślne ustawienia DRF są dość liberalne, jeśli samodzielnie nie zdefiniujesz nic w REST_FRAMEWORK. Przykładowo, brak DEFAULT_PERMISSION_CLASSES skutkuje tym, że domyślnie używane jest AllowAny. W praktyce oznacza to publiczny dostęp do wszystkich endpointów, jeśli nie ustawisz uprawnień per widok.
Bezpieczniejszy schemat to:
- ustawić globalnie
IsAuthenticated, - świadomie oznaczać publiczne endpointy (np. rejestracja, logowanie, healthcheck) jako
AllowAny.
Przykład globalnej konfiguracji wymuszającej logowanie:
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
}
Dzięki temu nawet jeśli ktoś zapomni ustawić permissions w nowym widoku, będzie on domyślnie chroniony.
Globalne a lokalne ustawienia autoryzacji i uwierzytelniania
Istnieją trzy główne poziomy konfiguracji:
- Globalna – w
REST_FRAMEWORK; wpływa na wszystkie widoki. - Na klasie widoku – np.
permission_classes = [... ]wAPIViewlubViewSet. - Dynamiczna – nadpisanie metod
get_permissions()lubget_authenticators().
Dynamiczne nadpisanie przydaje się, gdy np. w tym samym widoku chcesz zezwolić anonimom na GET, ale wymagać logowania dla POST:
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.viewsets import ModelViewSet
class TaskViewSet(ModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer
def get_permissions(self):
if self.request.method == 'GET':
return [AllowAny()]
return [IsAuthenticated()]
Taki kod trzeba stosować ostrożnie. Łatwo przeoczyć nowe metody (np. PATCH) lub nie uwzględnić akcji list/retrieve w ViewSet. Przy rozbudowanych warunkach lepiej trzymać logikę w osobnych klasach permissions.
request.user i request.auth w widokach oraz serializerach
Po przejściu uwierzytelniania DRF ustawia request.user i request.auth. W widokach można z tego korzystać wprost:
class ProjectListView(generics.ListCreateAPIView):
serializer_class = ProjectSerializer
def get_queryset(self):
return Project.objects.filter(members=self.request.user)
request.auth zawiera zwykle zawartość tokenu (np. instancję modelu tokenu lub obiekt reprezentujący JWT). Trzeba traktować go jako „dodatkowe dane uwierzytelnienia”, ale autoryzację opierać przede wszystkim na request.user i relacjach w bazie.
W serializerach dostęp do użytkownika realizuje się przez kontekst:
class TaskSerializer(serializers.ModelSerializer):
class Meta:
model = Task
fields = ['id', 'title', 'project', 'owner']
def create(self, validated_data):
validated_data['owner'] = self.context['request'].user
return super().create(validated_data)
W widoku trzeba wtedy przekazać kontekst (dla generics dzieje się to automatycznie). Jeśli tworzysz własne widoki oparte o APIView, warto nadpisać get_serializer_context(), aby w każdym serializerze mieć dostęp do bieżącego użytkownika.
Wbudowane klasy uwierzytelniania: kiedy wystarczą, kiedy nie
SessionAuthentication vs BasicAuthentication – zalety i ograniczenia
SessionAuthentication opiera się na mechanizmie sesji Django i ciasteczkach. Jest naturalnym wyborem dla klasycznych aplikacji webowych i panelu admina. Obsługuje CSRF i integruje się z LoginView oraz LogoutView. Dobrze działa z DRF, gdy:
- klientem jest przeglądarka,
- CSRF jest poprawnie skonfigurowany,
- backend i frontend są na tej samej domenie (lub domenach współdzielących cookies).
BasicAuthentication jest prostszy: klient wysyła parę login/hasło w nagłówku Authorization: Basic ... przy każdym żądaniu. Nie wymaga sesji ani cookies, ale dane są przesyłane za każdym razem – konieczne jest HTTPS. BasicAuth sprawdza się:
- w prostych integracjach serwer–serwer,
- przy wewnętrznych narzędziach o małej liczbie użytkowników,
- jako tymczasowe rozwiązanie dla testów.
Dla publicznych API używanych przez przeglądarkę i mobilki BasicAuth jest zbyt prosty i niewygodny – lepiej postawić na tokeny lub JWT.
TokenAuthentication – generowanie, rotacja, wygasanie
Klasyczny TokenAuthentication z DRF wykorzystuje model Token i jest bardzo łatwy do wdrożenia. Tworzysz token po zalogowaniu, a klient wysyła go w każdym żądaniu:
from rest_framework.authtoken.models import Token
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.response import Response
class CustomAuthToken(ObtainAuthToken):
def post(self, request, *args, **kwargs):
serializer = self.serializer_class(data=request.data,
context={'request': request})
serializer.is_valid(raise_exception=True)
user = serializer.validated_data['user']
token, created = Token.objects.get_or_create(user=user)
return Response({'token': token.key})
Z punktu widzenia bezpieczeństwa pojawiają się trzy kwestie:
- Brak wbudowanego wygaszania – tokeny są ważne dopóki ręcznie ich nie usuniesz. Dla projektów produkcyjnych lepiej wdrożyć mechanizm rotacji i wygaszania (np. własne pole
expires_atlub przejście na JWT). - Jeden token na użytkownika – domyślne API tworzy jeden token. Przy kilku urządzeniach jest to niewygodne (brak rozróżnienia który token jest z którego urządzenia).
- Utrata tokenu – jeśli ktoś przechwyci token, ma dostęp do konta dopóki token nie zostanie usunięty w bazie.
TokenAuthentication jest dobry na „prosty start” lub do integracji serwer–serwer w kontrolowanym środowisku, ale przy poważniejszych aplikacjach mobilnych warto przejść na JWT lub OAuth2.
Integracja z systemem logowania Django, CSRF i cookies
Django ma rozbudowany system logowania oparty na klasach widoków (LoginView, LogoutView) oraz mechanizm CSRF. W projektach, gdzie backend obsługuje również HTML, często łączy się klasyczne widoki HTML z API DRF.
Przykładowe podejście:
- Formularz logowania obsługuje
LoginView(sesja + cookie), - Panel admina korzysta z
SessionAuthenticationi CSRF, - API publiczne (dla SPA) korzysta z JWT lub tokenów, niezależnie od sesji przeglądarki.
CustomAuthentication – kiedy trzeba wyjść poza standard
Czasem gotowe klasy uwierzytelniania to za mało. Typowe powody tworzenia własnej klasy to:
- integracja z zewnętrznym IdP (np. system SSO w firmie),
- niestandardowy format nagłówka
Authorization, - własne reguły walidacji tokenu (np. dodatkowe flagi bezpieczeństwa, IP, agent).
Własna klasa uwierzytelniania zwykle dziedziczy po BaseAuthentication i implementuje metodę authenticate(self, request). Przykład prostej integracji z kluczem API w nagłówku:
from rest_framework.authentication import BaseAuthentication
from rest_framework import exceptions
from .models import ApiKey
class ApiKeyAuthentication(BaseAuthentication):
keyword = 'Api-Key'
def authenticate(self, request):
auth = request.headers.get('Authorization')
if not auth:
return None # brak nagłówka = brak próby uwierzytelnienia
try:
prefix, key = auth.split(' ', 1)
except ValueError:
raise exceptions.AuthenticationFailed('Invalid Authorization header')
if prefix != self.keyword:
return None # inny typ auth, niech obsłużą inne klasy
try:
api_key = ApiKey.objects.select_related('user').get(key=key, is_active=True)
except ApiKey.DoesNotExist:
raise exceptions.AuthenticationFailed('Invalid API key')
return (api_key.user, api_key)
Taka klasa może działać obok JWT lub sesji. Wystarczy dodać ją do listy globalnych klas uwierzytelniania albo użyć tylko w wybranych widokach:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'path.to.ApiKeyAuthentication',
],
}
Przy tworzeniu własnej autentykacji warto zdefiniować prosty protokół rotacji oraz wygaszania kluczy i tokenów. Bez tego API szybko zamienia się w „dostęp na zawsze”.

Tokeny JWT w DRF – konfiguracja i praktyczne pułapki
Wybór biblioteki: djangorestframework-simplejwt i alternatywy
Najczęściej używanym rozszerzeniem do JWT w DRF jest obecnie djangorestframework-simplejwt. Zapewnia:
- krótkotrwały access token i długotrwały refresh token,
- wbudowane endpointy do logowania, odświeżania, rotacji tokenów,
- obsługę blacklisty tokenów (opcjonalnie),
- rozszerzalne claims w tokenie.
Alternatywy (np. stare pakiety django-rest-framework-jwt) są mniej aktywnie rozwijane. Dla nowych projektów lepiej startować na simplejwt.
Podstawowa konfiguracja SimpleJWT
Instalacja i włączenie w projekcie przebiega w kilku krokach:
pip install djangorestframework-simplejwt
W settings.py dodajesz konfigurację JWT oraz klasę uwierzytelniania:
from datetime import timedelta
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
}
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15),
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
'AUTH_HEADER_TYPES': ('Bearer',),
}
Następnie podłączasz gotowe widoki do routingu:
from django.urls import path
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
urlpatterns = [
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]
Po zalogowaniu klient dostaje parę: access i refresh. Access token trafia do nagłówka Authorization: Bearer <token>, refresh przechowujesz w bezpieczniejszym miejscu (np. httpOnly cookie).
Rozszerzanie payloadu JWT o dodatkowe informacje
Często w JWT potrzebne są dodatkowe dane o użytkowniku: rola, ID organizacji, feature flagi. W simplejwt robi się to przez własny serializer:
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
@classmethod
def get_token(cls, user):
token = super().get_token(user)
# dodatkowe pola
token['email'] = user.email
token['is_staff'] = user.is_staff
token['org_id'] = getattr(user, 'organization_id', None)
return token
oraz własny widok:
from rest_framework_simplejwt.views import TokenObtainPairView
class CustomTokenObtainPairView(TokenObtainPairView):
serializer_class = CustomTokenObtainPairSerializer
i podmianę w URL-ach:
urlpatterns = [
path('api/token/', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'),
]
W payloadzie nie należy umieszczać danych wrażliwych (hasła, tokeny zewnętrzne, pełne dane osobowe). Payload JWT jest tylko zakodowany, nie zaszyfrowany – każdy posiadacz tokenu może go odczytać.
Przechowywanie JWT w frontendzie: nagłówek vs cookies
Frontendy webowe najczęściej trzymają access token w pamięci aplikacji (np. Redux, React Query) i doklejają go do nagłówka Authorization. Refresh token może być przechowywany:
- w
httpOnlycookie – lepsza ochrona przed XSS, ale trzeba uwzględnić CORS iSameSite, - w
localStorage– prostsze, ale bardziej podatne na XSS (przy słabszym zabezpieczeniu frontendu).
Popularny wariant: access token w pamięci (ramka aplikacji), refresh w cookie httpOnly na subdomenie API. Backend przy odświeżaniu odczytuje cookie, wydaje nowy access, a stary refresh trafia do blacklisty.
Blacklisting i rotacja tokenów
Sam expiry nie wystarcza, jeśli użytkownik wyloguje się lub zgłosi kradzież urządzenia. Wtedy potrzebne jest unieważnienie tokenu przed jego naturalnym wygaśnięciem. simplejwt ma moduł blacklisty.
Konfiguracja polega na włączeniu aplikacji i blacklisted tokens:
INSTALLED_APPS = [
# ...
'rest_framework_simplejwt.token_blacklist',
]
W ustawieniach SIMPLE_JWT można wymusić rotację:
SIMPLE_JWT = {
# ...
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
}
Przy każdym odświeżeniu klient dostaje nowy refresh token, a stary trafia do blacklisty. To ogranicza skutki wycieku, ale wymaga poprawnego przechowywania nowego refresh tokenu na froncie.
Typowe pułapki przy JWT
Najczęściej spotykane problemy:
- Zbyt długi czas życia access tokenu – wygodny dla klienta, ale ryzykowny przy kradzieży; sensowny zakres to minuty, nie dni.
- Brak lub błędna obsługa odświeżania – frontend ignoruje 401 i nie próbuje użyć refresh tokenu, przez co użytkownik „magicznie” wypada z sesji.
- Modyfikowanie uprawnień bez unieważnienia tokenów – jeśli rola użytkownika zmienia się w bazie, a token jeszcze obowiązuje, stara rola dalej daje dostęp. Tu pomaga krótkie TTL access tokenu plus mechanizmy revokacji przy krytycznych zmianach.
- Debugowe SECRET_KEY na produkcji – jeśli klucz do podpisu JWT wycieknie, trzeba unieważnić wszystkie tokeny i zmienić klucz globalnie.
Przy wdrażaniu JWT dobrze jest mieć spisane procedury: jak unieważnić wszystkie tokeny danego użytkownika, jak masowo wyłączyć dostęp (np. przy incydencie bezpieczeństwa), jak testować ścieżki wygasania.
Precyzyjna autoryzacja: Permission classes i Object-level permissions
Własne klasy Permission dla reguł biznesowych
Autoryzacja na poziomie endpointu opiera się na metodach has_permission() i has_object_permission(). Prosta reguła „tylko właściciel może edytować, ale każdy zalogowany może czytać” może wyglądać tak:
from rest_framework.permissions import BasePermission, SAFE_METHODS
class IsOwnerOrReadOnly(BasePermission):
def has_permission(self, request, view):
# dostęp do listy/utworzenia tylko dla zalogowanych
return request.user and request.user.is_authenticated
def has_object_permission(self, request, view, obj):
if request.method in SAFE_METHODS:
return True
return obj.owner == request.user
Klasa trafia do widoku lub viewsetu:
class TaskViewSet(ModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer
permission_classes = [IsOwnerOrReadOnly]
Takie podejście nadaje się do prostych polityk. Przy bardziej złożonych (dziesiątki ról, wiele organizacji) lepiej wyodrębnić warstwę domenową, a klasę permission zrobić jako cienki adapter wywołujący metody z tej warstwy.
Object-level permissions w praktyce
Object-level permissions są wywoływane po pobraniu obiektu. DRF stosuje je m.in. przy akcjach retrieve, update, destroy. Dla list (list) w grę wchodzi has_permission(), a sama lista powinna być przefiltrowana w get_queryset().
Typowy wzorzec:
class ProjectPermission(BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in SAFE_METHODS:
return obj.members.filter(pk=request.user.pk).exists()
return obj.owner == request.user
i jednocześnie odpowiednie zawężenie querysetu:
class ProjectViewSet(ModelViewSet):
serializer_class = ProjectSerializer
permission_classes = [ProjectPermission]
def get_queryset(self):
return Project.objects.filter(members=self.request.user)
Permissions chronią pojedynczy obiekt, a queryset pilnuje, żeby użytkownik w ogóle nie widział cudzych rekordów w odpowiedzi.
Łączenie wielu klas Permission
DRF łączy klasy permission logicznym AND: wszystkie muszą zwrócić True. Jeśli potrzeba logiki OR (np. „czy jest adminem, albo właścicielem”), można:
- napisać jedną złożoną klasę,
- użyć wzorca kompozycji wewnątrz klasy.
Przykład drugiego podejścia:
from rest_framework.permissions import IsAdminUser
class IsOwnerOrAdmin(BasePermission):
def has_object_permission(self, request, view, obj):
if IsAdminUser().has_permission(request, view):
return True
return obj.owner == request.user
Taki układ trzyma złożoność w jednym miejscu, zamiast budować skomplikowane if-y w samym widoku.
Filtrowanie dostępu w QuerySet i widokach – logika bezpieczeństwa bliżej danych
Wymuszanie filtrów na poziomie QuerySet
Permissions blokują operacje, ale nie filtrują automatycznie danych. Najczęstszy błąd to:
class InvoiceViewSet(ModelViewSet):
queryset = Invoice.objects.all()
serializer_class = InvoiceSerializer
permission_classes = [IsAuthenticated]
Taki kod przyznaje każdemu zalogowanemu dostęp do wszystkich faktur. Bezpieczniejsza wersja:
class InvoiceViewSet(ModelViewSet):
serializer_class = InvoiceSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return Invoice.objects.filter(customer=self.request.user)
Logikę filtrowania można przenieść do menedżerów modelu lub metod klasowych, by używać jej konsekwentnie:
class InvoiceQuerySet(models.QuerySet):
def for_user(self, user):
return self.filter(customer=user)
class Invoice(models.Model):
# ...
objects = InvoiceQuerySet.as_manager()
def get_queryset(self):
return Invoice.objects.for_user(self.request.user)
Integracja z django-filter i filtrowanie parametrami
Jeśli API pozwala filtrować wyniki parametrami ?status=, ?from= itd., wygodnie jest użyć django-filter. Kluczowe jest to, by filtr użytkownika był nakładany na bazowy, zawężony queryset, a nie odwrotnie.
import django_filters
from django_filters.rest_framework import DjangoFilterBackend
class InvoiceFilter(django_filters.FilterSet):
status = django_filters.CharFilter(field_name='status')
created_from = django_filters.DateFilter(field_name='created_at', lookup_expr='gte')
class Meta:
model = Invoice
fields = ['status', 'created_from']
class InvoiceViewSet(ModelViewSet):
serializer_class = InvoiceSerializer
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend]
filterset_class = InvoiceFilter
def get_queryset(self):
return Invoice.objects.for_user(self.request.user)
Najpierw for_user() ogranicza wyniki do faktur użytkownika, a dopiero potem filtr z żądania doprecyzowuje listę.
Bezpieczne łączenie złożonych filtrów i uprawnień
Przy bardziej złożonych scenariuszach (np. widok raportów z możliwością filtrowania po organizacji, projekcie, statusie) dobrze jest wprowadzić warstwę serwisową:
class ReportService:
@staticmethod
def get_reports_for_user(user):
qs = Report.objects.all()
if user.is_superuser:
return qs
if user.is_manager:
return qs.filter(organization=user.organization)
return qs.filter(owner=user)
class ReportViewSet(ReadOnlyModelViewSet):
serializer_class = ReportSerializer
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend]
filterset_class = ReportFilter
def get_queryset(self):
return ReportService.get_reports_for_user(self.request.user)
W ten sposób logika „kto co widzi” jest w jednym miejscu i można ją łatwiej testować jednostkowo.
Paginacja w DRF – mechanizmy, konfiguracja i wybór strategii
Najczęściej zadawane pytania (FAQ)
Jakie uwierzytelnianie wybrać w Django REST Framework dla SPA i aplikacji mobilnej?
W typowym zestawie SPA + aplikacja mobilna najczęściej kończy się na JWT (np. djangorestframework-simplejwt) albo OAuth2 (np. django-oauth-toolkit). Oba podejścia dobrze współpracują z frontami działającymi w innej domenie niż backend i nie wymagają sesji przeglądarkowych.
Prosty scenariusz: SPA loguje się po JWT, przechowuje access token w pamięci (np. in-memory lub httpOnly cookie), a mobilka – w bezpiecznym storage (Keychain/Keystore). Do tego dochodzi refresh token po stronie backendu, który pozwala rzadko wymuszać logowanie użytkownika, a jednocześnie wygaszać dostęp przy utracie urządzenia.
Czym różni się authentication, authorization i throttling w Django REST Framework?
Authentication odpowiada na pytanie „kto to jest?”. To etap, na którym DRF ustawia request.user i request.auth na podstawie np. nagłówka Authorization, ciasteczka sesyjnego czy tokenu. Za ten etap odpowiada lista authentication_classes.
Authorization (permissions) sprawdza „czy ten użytkownik może to zrobić?”. Tutaj wchodzą w grę permission_classes i ewentualnie uprawnienia obiektowe. Throttling to osobny mechanizm do ograniczania liczby żądań z danego źródła (użytkownik, IP, token), przydatny przy ochronie logowania, endpointów publicznych czy API dla aplikacji mobilnych.
Jak bezpiecznie ustawić domyślne uprawnienia w Django REST Framework?
Podstawowe zabezpieczenie to globalne wymuszenie logowania i jawne oznaczanie wyjątków. W settings.py warto ustawić:
REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': ['rest_framework.permissions.IsAuthenticated'], }
Po takim ustawieniu wszystkie widoki DRF domyślnie wymagają uwierzytelnienia. Publiczne endpointy (logowanie, rejestracja, healthcheck) oznacza się wtedy explicite przez permission_classes = [AllowAny] na poziomie widoku lub viewsetu.
Jaką paginację wybrać w Django REST Framework: PageNumber, LimitOffset czy Cursor?
PageNumberPagination jest najprostsza i intuicyjna dla paneli administracyjnych (parametr ?page=2). Dobrze działa, gdy liczba rekordów jest umiarkowana, a kolejność wyników rzadko się zmienia. To rozsądny wybór dla prostych paneli wewnętrznych.
LimitOffsetPagination (?limit=20&offset=40) daje większą kontrolę frontendowi, ale przy dużych tabelach i częstych zmianach kolejności potrafi być kosztowna i niestabilna. CursorPagination lepiej znosi duże wolumeny danych i zmiany w zbiorze (stabilniejsze wyniki, brak „przeskoków” po deployu), więc częściej wybierana jest dla obciążonych API mobilnych.
Jak testować autoryzację w Django REST Framework (permissions, JWT, role)?
Podstawowy zestaw testów autoryzacji to:
- sprawdzenie, że niezalogowany użytkownik dostaje 401/403 tam, gdzie wymagane jest logowanie,
- weryfikacja, że użytkownik bez uprawnień dostaje 403 przy próbie edycji lub usunięcia zasobu,
- testy „szczęśliwej ścieżki”, czyli że właściciel obiektu lub administrator projektu może wykonać operację.
W praktyce wygodnie jest przygotować fabryki/fixtury użytkowników (zwykły user, admin, członek projektu) oraz helper do logowania i generowania tokenów JWT. Dzięki temu można szybko pokryć różne scenariusze: dostęp do własnych zadań, zadań w cudzym projekcie, zasobów publicznych i prywatnych.
Jak w DRF rozdzielić konfigurację globalną i lokalną authentication/permissions?
Globalne ustawienia w REST_FRAMEWORK definiują domyślne klasy uwierzytelniania i uprawnień dla całego API. To miejsce na „bezpieczny default”, np. IsAuthenticated i zestaw obsługiwanych metod uwierzytelniania (Session, Token, JWT).
Na poziomie widoku lub viewsetu można te ustawienia nadpisać przez pola authentication_classes i permission_classes, a w bardziej złożonych przypadkach – przez get_authenticators() i get_permissions(). Daje to elastyczność, np. publiczny GET i zabezpieczony POST w tym samym endpointzie, przy zachowaniu spójnej, konserwatywnej polityki globalnej.
Jak zaprojektować uprawnienia obiektowe dla projektów i zadań w DRF?
Dla domeny typu „projekty i zadania” sprawdza się układ: członek projektu widzi wszystkie zadania w projekcie, ale edytować może tylko właściciel zadania lub administrator projektu. Technicznie realizuje się to przez własne klasy permissions.BasePermission i sprawdzanie relacji w has_object_permission().
Prosty schemat: w has_permission() filtrujesz dostęp do list (czy user jest członkiem projektu), a w has_object_permission() – do pojedynczych obiektów (czy jest właścicielem zadania albo adminem projektu). Taki podział pozwala utrzymać czytelny kod, który da się łatwo rozszerzyć o kolejne role, np. „gość tylko do odczytu”.






