Token, JWT, OAuth2, session auth, permissions, throttling
В DRF важно держать в голове три разных вопроса, которые часто путают:
request.user.💡 Правило: для браузерного SPA на том же домене — Session-аутентификация (cookie + CSRF). Для мобильных и сторонних клиентов — JWT или Token. Не смешивайте без необходимости: каждый дополнительный механизм — это поверхность атаки.
| Механизм | Состояние | Где хранится | Когда применять |
|---|---|---|---|
| SessionAuthentication | Stateful | Cookie + сессия в БД | SPA/браузер на своём домене |
| TokenAuthentication | Stateful | Токен в таблице БД | Простые API, один токен на юзера |
| JWT (SimpleJWT) | Stateless | Подписанный токен у клиента | Мобильные, микросервисы, масштаб |
| OAuth2 | Stateful | Через провайдера | «Войти через Google», сторонние интеграции |
Аутентификация настраивается глобально и переопределяется на уровне view:
# settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated', # безопасный дефолт
],
}⚠️ Дефолтное
DEFAULT_PERMISSION_CLASSESв DRF —AllowAny. Это значит, что забыв указать permission, вы открываете эндпоинт всему миру. СтавьтеIsAuthenticatedглобально и явно ослабляйте там, где нужно.
Простейший stateless-для-клиента механизм: один долгоживущий токен на пользователя, хранится в БД.
INSTALLED_APPS += ['rest_framework.authtoken']
# python manage.py migrate
from rest_framework.authtoken.models import Token
token, _ = Token.objects.get_or_create(user=user)
# Клиент шлёт: Authorization: Token 9944b09199c62bcf9418ad...⚠️ Минусы: токен нельзя отозвать иначе как удалив (нет срока жизни), один токен на пользователя, утечка = постоянный доступ. Для serious-проектов предпочтительнее JWT с коротким сроком жизни.
JWT — stateless: сервер не хранит токены, а проверяет подпись. Это масштабируется (любой инстанс проверит токен без обращения в БД), но добавляет нюансы с отзывом.
# settings.py
from datetime import timedelta
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15), # короткий!
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
'ROTATE_REFRESH_TOKENS': True, # новый refresh при обновлении
'BLACKLIST_AFTER_ROTATION': True, # старый refresh — в чёрный список
'AUTH_HEADER_TYPES': ('Bearer',),
}# urls.py
from rest_framework_simplejwt.views import (
TokenObtainPairView, TokenRefreshView, TokenBlacklistView,
)
urlpatterns = [
path('api/token/', TokenObtainPairView.as_view()), # login → access + refresh
path('api/token/refresh/', TokenRefreshView.as_view()), # обновить access
path('api/token/blacklist/', TokenBlacklistView.as_view()), # logout
]| Токен | Срок жизни | Назначение |
|---|---|---|
| Access | Минуты | Прикладывается к каждому запросу. Короткий, чтобы утечка была не страшна |
| Refresh | Дни/недели | Только для получения нового access. Хранится максимально безопасно |
⚠️ Проблема отзыва JWT: подписанный access-токен валиден до истечения, даже если вы заблокировали пользователя. Поэтому access короткоживущий, а отзыв реализуется через blacklist refresh-токенов (
rest_framework_simplejwt.token_blacklist). Полностью stateless и при этом отзываемый — взаимоисключающие требования.
💡 Где хранить JWT на фронте:
localStorageуязвим к XSS. Безопаснее — refresh вHttpOnly-cookie, access в памяти JS. Это компромисс между UX и безопасностью.
Permission-классы решают, пропустить ли запрос. Встроенные:
| Класс | Доступ |
|---|---|
AllowAny | Всем |
IsAuthenticated | Только залогиненным |
IsAdminUser | Только is_staff |
IsAuthenticatedOrReadOnly | Чтение всем, запись — залогиненным |
DjangoModelPermissions | По правам модели Django (add/change/delete) |
Глобальный permission говорит «можно работать с постами вообще». Object-level — «можно ли тебе трогать этот пост»:
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS: # GET/HEAD/OPTIONS
return True
return obj.author == request.user # менять — только владельцуclass PostViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly]⚠️
has_object_permissionвызывается только при доступе черезget_object()(detail-вьюхи). Для list-эндпоинтов он не срабатывает — фильтруйте queryset вручную (Post.objects.filter(author=request.user)), иначе чужие объекты попадут в выдачу.
Троттлинг ограничивает частоту запросов — спасает от брутфорса, скрейпинга и случайного DDoS «своими» клиентами.
# settings.py
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle',
],
'DEFAULT_THROTTLE_RATES': {
'anon': '20/hour', # неавторизованные — жёстко
'user': '1000/day', # авторизованные — свободнее
'login': '5/min', # отдельная политика для логина
},
}# Кастомный троттл для чувствительного эндпоинта
from rest_framework.throttling import ScopedRateThrottle
class LoginView(APIView):
throttle_classes = [ScopedRateThrottle]
throttle_scope = 'login' # использует ставку 'login': '5/min'💡 Троттлинг DRF хранит счётчики в кэше (
CACHES). Для production обязательно настройте Redis-кэш — наLocMemCacheсчётчики у каждого воркера свои, и лимит фактически умножается на число процессов.
# api/permissions.py
from rest_framework import permissions
class IsOwner(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return obj.owner_id == request.user.id# api/views.py
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from .permissions import IsOwner
class NoteViewSet(viewsets.ModelViewSet):
serializer_class = NoteSerializer
permission_classes = [IsAuthenticated, IsOwner]
def get_queryset(self):
# list тоже отдаёт только свои заметки (object-perm для list не работает!)
return Note.objects.filter(owner=self.request.user)
def perform_create(self, serializer):
serializer.save(owner=self.request.user) # владелец — текущий юзер# api/urls.py
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
urlpatterns = [
path('auth/login/', TokenObtainPairView.as_view()),
path('auth/refresh/', TokenRefreshView.as_view()),
path('notes/', NoteViewSet.as_view({'get': 'list', 'post': 'create'})),
]AllowAny, и ставите IsAuthenticated глобальноВопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.
Далее: Кэширование с Redis