ПРАКТИЧЕСКИЙ РАЗБОР · 7 минут

Как устроена JWT-авторизация и почему API перестаёт пускать пользователя

JWT access и refresh, защищённый endpoint Django REST Framework и причины ответа 401

Пользователь вошёл в приложение, открыл личный кабинет и начал работать. Через некоторое время запрос к API возвращает 401 Unauthorized. При этом страница по-прежнему показывает, что пользователь вошёл в систему. Такое часто происходит, когда frontend и backend по-разному понимают состояние авторизации.

JWT помогает передавать подтверждение входа между запросами. Но у токенов есть срок действия, а доступ к защищённым данным должен проверяться сервером. Разберём, зачем нужны access и refresh, на примере Django REST Framework.

Зачем нужен JWT

После успешного входа сервер может выдать токен. В последующих запросах клиент передаёт его в заголовке Authorization. Сервер проверяет подпись токена, срок действия и другие условия, а затем определяет, от имени какого пользователя пришёл запрос.

JWT состоит из трёх частей, разделённых точками. В нём могут быть данные о пользователе и сроке действия, но это не зашифрованный сейф. Содержимое обычного JWT можно прочитать после декодирования. Поэтому туда не помещают пароль или другие секреты.

В нашем примере используется пара токенов. Access token даёт доступ к API и обычно живёт недолго. Refresh token позволяет получить новый access token без повторного ввода пароля. Такой механизм есть в библиотеке Simple JWT для Django REST Framework.

Первый защищённый запрос

В Django можно подключить Simple JWT и указать DRF, как проверять входящие запросы.

pip install djangorestframework-simplejwt
# settings.py
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": (
        "rest_framework_simplejwt.authentication.JWTAuthentication",
    ),
}

Затем добавим защищённую страницу API. Она возвращает короткое приветствие только пользователю с действующим токеном.

# views.py
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response

@api_view(["GET"])
@permission_classes([IsAuthenticated])
def profile(request):
    return Response({"message": f"Привет, {request.user.username}"})

JWTAuthentication определяет пользователя, а IsAuthenticated запрещает доступ анонимному запросу. Это разные задачи. Проверка личности сама по себе не означает разрешение на просмотр любого ресурса.

Для получения и обновления токенов у Simple JWT есть готовые обработчики. Пути в примере задаём сами.

# urls.py
from django.urls import path
from rest_framework_simplejwt.views import (
    TokenObtainPairView,
    TokenRefreshView,
)
from .views import profile

urlpatterns = [
    path("api/token/", TokenObtainPairView.as_view()),
    path("api/token/refresh/", TokenRefreshView.as_view()),
    path("api/profile/", profile),
]

После успешного входа POST /api/token/ возвращает access и refresh. Полученный access token можно передать в запросе к профилю.

GET /api/profile/ HTTP/1.1
Authorization: Bearer <access_token>

Настоящий токен в примере заменён обозначением. Запрос без действующего access token не должен возвращать защищённые данные.

Что происходит, когда access token истёк

Сначала всё работает. Потом срок действия access token заканчивается. Следующий запрос получает отказ, хотя пользователь не нажимал кнопку выхода. Это нормальная ситуация для схемы с короткоживущим access token.

Клиент может обратиться к POST /api/token/refresh/, передав действующий refresh token, получить новый access и повторить исходный запрос один раз. Если refresh уже недействителен, пользователь должен пройти вход заново.

Здесь легко допустить ошибку. Например, бесконечно повторять запрос после 401 или пытаться обновлять токен для каждого обращения к API. Лучше иметь одну точку, отвечающую за обновление, и ограничивать число повторных попыток.

Другой частый перекос возникает, когда интерфейс хранит собственное состояние входа, а API живёт по времени действия JWT. Поэтому надпись «Вы вошли» в браузере не заменяет серверную проверку доступа.

Где должны находиться токены

Токены дают доступ к данным, поэтому их хранение и передача требуют отдельного решения. Нельзя выводить их в журналы ошибок или передавать случайным внешним сервисам. Для веб-приложения с Next.js возможна схема, в которой серверная сессия управляет токенами Django, а браузер не обращается к секретам напрямую.

Полная связка NextAuth, Django JWT, обновления токенов и Google OAuth разобрана в моей статье на Habr. Здесь рассмотрим отдельную задачу, как JWT защищает API и что делать после истечения access token.

Когда одной формы входа уже недостаточно

После авторизации в реальном приложении появляются личные данные, роли, подписки и ограничения доступа. Все они проверяются на backend, а не только скрываются кнопками в интерфейсе. JWT отвечает за подтверждение пользователя при запросе. Правила доступа к конкретным данным остаются отдельной частью приложения.

В демонстрационном AI-Chat можно увидеть, как выглядит пользовательский продукт с frontend и Django API. Курс «AI на Django и Next II» связывает авторизацию, API, AI-модели и деплой в одной последовательной разработке.

Начать можно с одного защищённого запроса и понять, почему он возвращает 401. После этого механизм обновления токенов перестаёт выглядеть отдельной загадкой.