Session/Token/JWT, OAuth2/OIDC, permissions и throttling
Статус: актуально для Django 5.2/6.0 и современных версий Django REST Framework. JWT и OAuth/OIDC реализуются сторонними пакетами и требуют сверки их совместимости отдельно.
В защите API участвуют три разных механизма:
request.user и request.auth;Наличие валидного токена ещё не даёт права изменить любой объект. И наоборот, throttling не доказывает личность и не является полноценной защитой от DDoS или перебора пароля.
По умолчанию DRF использует AllowAny. Для приватного продукта безопаснее закрыть API глобально, а публичные endpoint открывать явно:
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.SessionAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
}AllowAny не «плохой» класс: он нужен health check, публичному каталогу или регистрации. Важно, чтобы открытость была заметным решением конкретной view.
Порядок authentication classes имеет значение. DRF перебирает их по очереди, а первый подходящий механизм устанавливает пользователя. Первый класс также влияет на то, получит неаутентифицированный клиент 401 с WWW-Authenticate или 403.
| Механизм | Практическое применение | Важные ограничения |
|---|---|---|
SessionAuthentication | Браузерное приложение на доверенном домене | Cookie, сессия и CSRF-защита небезопасных методов |
BasicAuthentication | Диагностика или закрытая интеграция по TLS | Учётные данные передаются на каждом запросе; не типичный production login |
TokenAuthentication | Небольшой API с простым серверным отзывом | Один сохраняемый токен на пользователя, штатно без срока действия |
| JWT через Simple JWT | Мобильные и распределённые клиенты | Сложнее отзыв, ротация и безопасное хранение |
| OAuth 2.0 / OpenID Connect | Делегированный доступ и внешний вход | Это протокол и отдельная модель угроз, а не один класс DRF |
Не выбирайте JWT только потому, что приложение «масштабируется». Обычная Django session хорошо работает с общим хранилищем сессий и проще отзывается. JWT уменьшает часть серверного состояния, но обычно всё равно требует загрузить пользователя и не отменяет permissions.
Для браузерного клиента на том же сайте session cookie — естественный выбор. Браузер прикладывает cookie автоматически, поэтому небезопасные методы POST, PUT, PATCH и DELETE должны проходить CSRF-защиту.
SessionAuthentication проверяет CSRF для аутентифицированных запросов. Это не повод строить login endpoint, который «сначала анонимен, значит CSRF не нужен»: вход нужно выполнять стандартной Django view или собственной view с явной CSRF-защитой.
Клиент обычно получает CSRF cookie, читает доступный токен и отправляет его в заголовке X-CSRFToken. SameSite помогает, но не заменяет CSRF-проверку. Настройки Secure, HttpOnly, домена cookie и доверенных origins зависят от реальной схемы доменов и HTTPS.
Для токенов DRF нужно подключить приложение и миграции:
INSTALLED_APPS = [
# ...
"rest_framework.authtoken",
]
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.TokenAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
}После migrate клиент отправляет:
Authorization: Token 9944b09199c62bcf9418ad...Этот механизм обязан использоваться только по HTTPS. Штатный токен хранится в базе, связан с одним пользователем и не имеет встроенного срока действия. Его можно отозвать удалением и выдать заново, но для нескольких устройств, областей доступа и ротации часто нужен более развитый пакет или собственная модель credentials.
Не логируйте заголовок Authorization и не возвращайте токены в диагностических ошибках.
JWT содержит подписанные claims. Access token обычно живёт недолго и передаётся к API, refresh token живёт дольше и используется только для выпуска новой пары.
from datetime import timedelta
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=10),
"REFRESH_TOKEN_LIFETIME": timedelta(days=7),
"ROTATE_REFRESH_TOKENS": True,
"BLACKLIST_AFTER_ROTATION": True,
"AUTH_HEADER_TYPES": ("Bearer",),
}Для blacklist нужно не только включить настройку, но и установить приложение с миграциями:
INSTALLED_APPS = [
# ...
"rest_framework_simplejwt.token_blacklist",
]from django.urls import path
from rest_framework_simplejwt.views import (
TokenBlacklistView,
TokenObtainPairView,
TokenRefreshView,
)
urlpatterns = [
path("auth/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
path("auth/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
path("auth/token/revoke/", TokenBlacklistView.as_view(), name="token_revoke"),
]После изменения INSTALLED_APPS выполните миграции. Blacklist позволяет отозвать refresh/sliding tokens, но уже выданный access token обычно остаётся действительным до exp. Поэтому короткий срок access token — часть модели отзыва, а не косметическая настройка.
Ротация refresh token уменьшает ценность украденного старого токена, если старый сразу попадает в blacklist. Однако система должна определить поведение при повторном использовании, очистку истёкших записей и принудительный выход со всех устройств.
Универсально безопасного места нет:
localStorage доступен JavaScript и поэтому особенно уязвим при XSS;HttpOnly cookie не читается JavaScript, но браузер отправляет её автоматически, поэтому нужны CSRF-защита и продуманные cookie-атрибуты;Схема «refresh в HttpOnly cookie, access в памяти» распространена, но Simple JWT из коробки ориентирован на токены в запросе/ответе; cookie-вариант требует аккуратной собственной интеграции. Не отключайте CSRF только потому, что внутри cookie находится JWT.
Для мобильного приложения используют защищённое хранилище операционной системы. В любом варианте ограничивайте утечки через логи, аналитику, URL и сообщения об ошибках.
OAuth 2.0 отвечает прежде всего за делегированную авторизацию: клиент получает ограниченный доступ к ресурсу. OpenID Connect добавляет поверх OAuth слой идентификации пользователя и id_token. Поэтому кнопка «Войти через провайдера» обычно означает OIDC, даже если в разговоре её называют OAuth-входом.
Если приложение само становится OAuth provider, рассмотрите поддерживаемый пакет вроде Django OAuth Toolkit. Если оно принимает вход от внешнего identity provider, используйте библиотеку OIDC/social-auth с проверкой state, nonce, redirect URI, issuer и audience. Не реализуйте протокол вручную по нескольким примерам из блога.
Permission-классы проверяются до выполнения основного метода view. Часто используются:
AllowAny — endpoint открыт;IsAuthenticated — нужен аутентифицированный пользователь;IsAdminUser — нужен user.is_staff;IsAuthenticatedOrReadOnly — безопасные методы разрешены всем;DjangoModelPermissions — используются модельные add, change, delete и связанные права.Object permission отвечает за конкретную строку:
from rest_framework.permissions import BasePermission, SAFE_METHODS
class IsOwnerOrReadOnly(BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in SAFE_METHODS:
return True
return obj.owner_id == request.user.idfrom rest_framework.permissions import IsAuthenticated
class NoteViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
serializer_class = NoteSerializer
def get_queryset(self):
return Note.objects.filter(owner=self.request.user)
def perform_create(self, serializer):
serializer.save(owner=self.request.user)Пример списка приватных заметок требует аутентификацию для всей view. Если безопасные методы открыты анонимно, обращение к request.user нужно обработать отдельно. Главное правило неизменно: has_object_permission() стандартно вызывается на detail-объекте, но не на каждой строке list. Список фильтрует get_queryset().
Проверяйте permissions для всех действий: list, retrieve, create, оба обновления, удаление и каждый @action. Право на родительский объект не всегда означает право привязать к нему любой дочерний объект.
REST_FRAMEWORK = {
"DEFAULT_THROTTLE_CLASSES": [
"rest_framework.throttling.AnonRateThrottle",
"rest_framework.throttling.UserRateThrottle",
],
"DEFAULT_THROTTLE_RATES": {
"anon": "60/min",
"user": "1000/hour",
"login": "5/min",
},
}from rest_framework.throttling import ScopedRateThrottle
class LoginView(APIView):
throttle_classes = [ScopedRateThrottle]
throttle_scope = "login"Встроенный throttling DRF использует cache и не гарантирует строгое число запросов: операции счётчика могут быть неатомарными, а конкурентные запросы — немного превышать лимит. LocMemCache изолирован в каждом процессе, поэтому для нескольких workers нужен общий cache, например Redis.
IP-адрес тоже не является надёжной личностью: пользователи делят NAT, атакующий меняет адреса, а неверная конфигурация reverse proxy позволяет подделать исходный IP. Проверьте настройки proxy/NUM_PROXIES и применяйте разные ключи: IP, аккаунт, device/client и глобальный предел.
Троттлинг DRF полезен для политики использования и сдерживания случайной нагрузки. Защиту login/reset endpoint дополняют задержками, блокировками или challenge, наблюдаемостью и лимитами на edge/proxy. Объёмный DDoS должен останавливаться до Django.
Для каждого защищённого endpoint тестируйте матрицу:
Проверяйте не только HTTP-код, но и отсутствие данных в теле. Для чувствительных объектов ответ 404 вместо 403 иногда уменьшает возможность перебора идентификаторов, однако политика должна быть последовательной.
Цель настройки не в том, чтобы подключить максимальное число механизмов. Хорошая схема коротко объясняет, где хранится credential, как он отзывается, как защищён браузерный запрос и где именно проверяется доступ к каждой строке данных.
Далее: Кэширование с Redis