Аутентификация и права
Кто пользователь (аутентификация) и что ему можно (права) — два независимых шага 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().