Аутентификация и права

Кто пользователь (аутентификация) и что ему можно (права) — два независимых шага DRF, которые часто путают.

Аутентификация отвечает на вопрос «кто это?» и заполняет request.user. Авторизация (права) отвечает на вопрос «можно ли этому пользователю выполнить запрос?». В DRF это два отдельных слоя: authentication_classes и permission_classes.

Зачем это знать на практике

Путаница этих двух слоёв — источник реальных уязвимостей. Классический промах: «я же требую токен, значит API защищён». Но токен лишь устанавливает личность; без проверки прав любой залогиненный пользователь сможет отредактировать чужую запись. Чёткое разделение «сначала узнаём кто, потом решаем что можно» даёт предсказуемую модель безопасности, где легко рассуждать о каждом эндпоинте.

Шаг 1. Аутентификация: TokenAuthentication

Самый простой способ — токены DRF. Клиент один раз присылает логин/пароль, получает строку-токен и дальше шлёт её в заголовке. Включается это в настройках и через приложение authtoken:

INSTALLED_APPS = [
    # ...
    "rest_framework",
    "rest_framework.authtoken",
]

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework.authentication.TokenAuthentication",
    ],
}

Клиент передаёт токен в заголовке Authorization:

GET /api/articles/ HTTP/1.1
Authorization: Token 9944b09199c62bcf9418 ...

DRF находит пользователя по токену и кладёт его в request.user. Если токена нет или он неверный — request.user станет AnonymousUser. Важно: сам по себе невалидный токен ещё не даёт 401 — отказ выдаст уже слой прав.

JWT: токен без хранения в базе

У DRF-токена есть минус: он один на пользователя и лежит в БД. JWT (JSON Web Token) устроен иначе — это самодостаточная подписанная строка с «полезной нагрузкой» (id пользователя, срок действия). Сервер не хранит JWT: он проверяет подпись своим секретом и доверяет содержимому. Обычно подключают пакет djangorestframework-simplejwt:

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework_simplejwt.authentication.JWTAuthentication",
    ],
}
# urls.py — эндпоинты получения и обновления пары токенов
from rest_framework_simplejwt.views import (
    TokenObtainPairView, TokenRefreshView,
)
urlpatterns += [
    path("api/token/", TokenObtainPairView.as_view()),
    path("api/token/refresh/", TokenRefreshView.as_view()),
]

JWT отдают парой: короткоживущий access-токен для запросов и долгоживущий refresh для получения нового access без повторного ввода пароля. Заголовок выглядит как Authorization: Bearer <access-токен>. Плюс — масштабируемость (сервер не ходит в БД на каждый запрос), минус — токен нельзя «отозвать» мгновенно, он действует до истечения срока.

Шаг 2. Права: permission_classes

После того как личность установлена, решают, пускать ли запрос. За это отвечают классы прав. Самый частый — IsAuthenticated: пускает только залогиненных.

from rest_framework.permissions import IsAuthenticated

class ArticleViewSet(viewsets.ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    permission_classes = [IsAuthenticated]

Готовые классы покрывают типовые случаи:

КлассКого пускает
AllowAnyвсех, включая анонимов
IsAuthenticatedтолько аутентифицированных
IsAdminUserтолько is_staff
IsAuthenticatedOrReadOnlyчитать всем, писать — залогиненным

Кастомные права: объектный уровень

Готовых классов не хватает, когда правило зависит от конкретного объекта: «редактировать статью может только её автор». Для этого пишут свой класс с методом has_object_permission:

from rest_framework import permissions

class IsAuthorOrReadOnly(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
        # читать можно всем
        if request.method in permissions.SAFE_METHODS:
            return True
        # менять — только автору
        return obj.author == request.user

У класса прав два уровня проверки: has_permission(request, view) — общий доступ к эндпоинту (до загрузки объекта), и has_object_permission(request, view, obj) — доступ к конкретной записи. Второй вызывается только для операций с одним объектом (retrieve/update/destroy) и только если вьюха обратилась к get_object().

class ArticleViewSet(viewsets.ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    permission_classes = [IsAuthenticatedOrReadOnly, IsAuthorOrReadOnly]

Список прав работает как «И»: запрос пройдёт, только если каждый класс вернул True.

Как это работает под капотом

Внутри dispatch() DRF идёт строго по порядку. Сначала perform_authentication перебирает authentication_classes: первый класс, опознавший запрос, задаёт request.user и request.auth; если ни один не сработал — пользователь анонимен (это ещё не ошибка). Затем check_permissions вызывает у каждого класса прав has_permission(); если хоть один вернул False, запрос отклоняется. Код отказа выбирается по тому, аутентифицирован ли пользователь: аноним получает 401 Unauthorized (намёк «представься»), а залогиненный без прав — 403 Forbidden («ты опознан, но нельзя»). Позже, когда вьюха зовёт get_object(), дополнительно срабатывает check_object_permissions с has_object_permission(). Поэтому объектные права не защитят список (list) — там get_object() не вызывается, и фильтровать выдачу нужно через get_queryset().

Частые ошибки

  • Считать аутентификацию защитой. Токен говорит «кто», но не «что можно». Без прав любой залогиненный правит чужое.
  • Полагаться на has_object_permission для списка. Для list объектный метод не вызывается; ограничивайте выборку в get_queryset().
  • Ждать мгновенного отзыва JWT. Самодостаточный токен действует до истечения срока; для отзыва нужны короткий TTL и blacklist refresh-токенов.
  • Путать 401 и 403. 401 — «не представился», 403 — «представился, но прав нет»; DRF выбирает их автоматически.
  • Хранить пароль вместо обмена на токен. Клиент шлёт логин/пароль один раз за токеном, дальше ходит с токеном, а не с паролем в каждом запросе.

Итоги

  • Аутентификация (authentication_classes) определяет request.user; авторизация (permission_classes) решает, пускать ли запрос.
  • TokenAuthentication хранит токен в БД; JWT самодостаточен и не требует обращения к базе, но не отзывается мгновенно.
  • IsAuthenticated, IsAuthenticatedOrReadOnly, IsAdminUser покрывают типовые случаи; список прав работает как «И».
  • Объектные правила («только автор») реализуют через has_object_permission в своём классе прав.
  • Аноним без прав получает 401, залогиненный без прав — 403; для списка фильтруйте get_queryset().
Проверьте себя
1. В чём разница между аутентификацией и правами (авторизацией) в DRF?
AЭто синонимы для одного и того же слоя
BАутентификация определяет, кто пользователь (request.user); права решают, что ему можно
CАутентификация про скорость, права про кеш
DПрава работают до аутентификации
2. Чем JWT отличается от обычного TokenAuthentication DRF?
AJWT обязательно хранится в базе данных
BJWT самодостаточен: сервер проверяет подпись и не хранит токен, но не может отозвать его мгновенно
CJWT нельзя передавать в заголовке
DJWT работает только для админов
3. Как ограничить редактирование статьи только её автором?
AПоставить permission_classes = [AllowAny]
BНаписать класс прав с методом has_object_permission, возвращающим obj.author == request.user
CВключить TokenAuthentication
DДобавить write_only=True в сериализатор