Как получить JWT токен в Django REST Framework?

JSON Web Token (JWT) стал стандартом де-факто для реализации stateless-аутентификации в современных веб-приложениях, особенно в SPA и мобильных клиентах, взаимодействующих с бэкендом через RESTful API. Django REST Framework (DRF) предоставляет гибкие механизмы для интеграции с различными схемами аутентификации, и JWT не исключение. В этой статье мы рассмотрим, как эффективно работать с JWT в DRF, используя популярную библиотеку djangorestframework-simplejwt.

Введение в JWT и Django REST Framework

Что такое JWT (JSON Web Token) и зачем он нужен?

JWT — это открытый стандарт (RFC 7519) для безопасной передачи информации между сторонами в виде объекта JSON. Этот объект обычно состоит из трех частей, разделенных точками:

Header: Содержит тип токена (JWT) и используемый алгоритм подписи (например, HS256, RS256).

Payload: Содержит набор утверждений (claims). Claims могут быть стандартными (например, iss — издатель, exp — время истечения), зарегистрированными, но необязательными (например, sub — субъект, aud — аудитория), или пользовательскими (произвольные данные).

Signature: Подпись, созданная из Header, Payload и секретного ключа (или пары ключей). Она используется для проверки целостности токена и подтверждения его подлинности.

Основное преимущество JWT в контексте аутентификации заключается в том, что он самодостаточен. После выдачи токена, серверу не требуется хранить информацию о сессии пользователя. Каждое последующее обращение с токеном может быть аутентифицировано путем проверки подписи токена, что идеально подходит для масштабируемых stateless API.

Преимущества использования JWT в Django REST Framework

Использование JWT с DRF дает несколько ключевых преимуществ:

Statelessness: Серверу не нужно хранить информацию о состоянии пользователя между запросами, что упрощает горизонтальное масштабирование.

Производительность: Аутентификация сводится к криптографической проверке токена, что обычно быстрее запросов к базе данных для проверки сессии или токенаopaque.

Подходит для API: Естественно интегрируется с архитектурой RESTful API, где клиент (SPA, мобильное приложение) получает токен после входа и затем использует его для доступа к защищенным ресурсам.

Возможность включения пользовательских данных: В Payload можно добавить базовую информацию о пользователе (например, ID, роль), чтобы избежать лишних запросов к БД при каждом запросе.

Обзор основных пакетов для работы с JWT в Django REST Framework

Существует несколько библиотек для работы с JWT в DRF. Наиболее популярными и активно поддерживаемыми являются:

djangorestframework-simplejwt: Простой в использовании, но мощный пакет, предоставляющий views и serializers для получения и обновления токенов. Поддерживает пары access/refresh токенов.

drf-jwt: Более старый пакет, но все еще используется во многих проектах. Также поддерживает пары токенов.

В данной статье мы сфокусируемся на djangorestframework-simplejwt как наиболее современном и рекомендованном решении.

Установка и настройка необходимых пакетов

Для работы с JWT в DRF, в первую очередь, необходимо установить соответствующий пакет и провести базовую настройку проекта.

Установка simple-jwt: pip install djangorestframework_simplejwt

Установка выполняется стандартным образом через pip:

pip install djangorestframework_simplejwt

Убедитесь, что вы используете виртуальное окружение.

Настройка settings.py для использования simple-jwt

После установки, необходимо зарегистрировать приложение rest_framework_simplejwt в списке INSTALLED_APPS в вашем файле settings.py:

# settings.py

INSTALLED_APPS = [
    # ... другие приложения
    'rest_framework',
    'rest_framework_simplejwt',
    # ...
]

Добавьте базовые настройки для simple-jwt в settings.py. Обязательно задайте ACCESS_TOKEN_LIFETIME и REFRESH_TOKEN_LIFETIME:

# settings.py

from datetime import timedelta

SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5), # Короткий срок жизни для access токена
    'REFRESH_TOKEN_LIFETIME': timedelta(days=1),   # Более долгий срок для refresh токена
    'ROTATE_REFRESH_TOKENS': False, # Включите, если хотите, чтобы refresh токен менялся при каждом использовании
    'BLACKLIST_AFTER_ROTATION': False, # Включите, если ROTATE_REFRESH_TOKENS = True
    'ALGORITHM': 'HS256',
    'SIGNING_KEY': SECRET_KEY, # Используйте SECRET_KEY вашего проекта
    'VERIFYING_KEY': None,
    'AUDIENCE': None,
    'ISSUER': None,
    'JWK_URL': None,
    'LEEWAY': 0,

    'AUTH_HEADER_TYPES': ('Bearer',), # Тип заголовка аутентификации
    'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
    'USER_ID_FIELD': 'id',
    'USER_ID_CLAIM': 'user_id',
    'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule',

    'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
    'TOKEN_TYPE_CLAIM': 'token_type',
    'TOKEN_USER_CLASS': 'rest_framework_simplejwt.models.TokenUser',

    'JTI_CLAIM': 'jti',

    'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5),
    'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1),
}

Это базовая конфигурация. Вы можете настроить многие другие параметры в соответствии с вашими требованиями безопасности и удобства.

Добавление аутентификации JWT по умолчанию

Чтобы DRF по умолчанию использовал JWT для аутентификации запросов к API views, настройте DEFAULT_AUTHENTICATION_CLASSES в settings.py:

# settings.py

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
        # 'rest_framework.authentication.SessionAuthentication', # Опционально, для browsable API
        # 'rest_framework.authentication.BasicAuthentication', # Опционально
    ),
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    )
}

Теперь все views, где не указаны другие классы аутентификации или разрешений, будут требовать действительный JWT токен в заголовке Authorization.

Получение JWT токена: основные способы

Получение токенов — это первый шаг. Пользователь отправляет свои учетные данные (например, логин и пароль), а сервер в ответ выдает пару токенов: access и refresh.

Использование стандартных views для получения токена (TokenObtainPairView, TokenRefreshView)

djangorestframework-simplejwt предоставляет готовые классы представлений (views) для этих операций:

TokenObtainPairView: Принимает логин и пароль пользователя, возвращает пару access и refresh токенов.

TokenRefreshView: Принимает refresh токен, возвращает новую пару access и refresh токенов.

Эти views используют стандартные сериализаторы TokenObtainPairSerializer и TokenRefreshSerializer соответственно.

Настройка URL endpoints для получения и обновления токенов

Для того чтобы эти views стали доступны по URL-адресам, добавьте их в ваш urls.py:

# urls.py

from django.urls import path
from rest_framework_simplejwt.views import (
    TokenObtainPairView,
    TokenRefreshView,
    # TokenVerifyView # Опционально, для проверки валидности токена
)

urlpatterns = [
    # ... другие URL'ы
    path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
    # path('api/token/verify/', TokenVerifyView.as_view(), name='token_verify'), # Опционально
    # ...
]

Теперь клиент может сделать POST-запрос на /api/token/ с JSON телом вида {"username": "your_username", "password": "your_password"} и получить в ответ {"access": "your_access_token", "refresh": "your_refresh_token"}. Запрос на /api/token/refresh/ с телом {"refresh": "your_refresh_token"} вернет новую пару токенов.

Кастомизация процесса получения токена: переопределение TokenObtainPairSerializer

Стандартный TokenObtainPairSerializer проверяет username и password. Если вы хотите использовать email или добавить дополнительные проверки при аутентификации, или включить пользовательские данные в Payload access токена, вам нужно переопределить этот сериализатор.

Создайте файл serializers.py в вашем приложении (или любом удобном месте) и унаследуйтесь от TokenObtainPairSerializer:

# users/serializers.py (пример)

from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework_simplejwt.tokens import AccessToken

class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
    @classmethod
    def get_token(cls, user) -> AccessToken:
        """
        Создает access токен для пользователя и добавляет кастомные клеймы.

        Args:
            user: Объект пользователя Django.

        Returns:
            Объект AccessToken с добавленными клеймами.
        """
        token: AccessToken = super().get_token(user)

        # Добавление пользовательских данных в payload токена (кастомные клеймы)
        token['username'] = user.username
        # Пример добавления роли, если у пользователя есть поле 'role'
        # if hasattr(user, 'role'):
        #     token['role'] = user.role

        return token

    # Если нужно кастомизировать аутентификацию (например, по email)
    # def validate(self, attrs: dict) -> dict:
    #     """
    #     Кастомная валидация учетных данных пользователя.
    #
    #     Args:
    #         attrs: Словарь с входными данными (например, 'username', 'password').
    #
    #     Returns:
    #         Словарь с парой токенов ('access', 'refresh').
    #
    #     Raises:
    #         AuthenticationFailed: Если учетные данные неверны.
    #     """
    #     # Пример аутентификации по email вместо username
    #     email = attrs.get('email')
    #     password = attrs.get('password')
    #
    #     if not email or not password:
    #          raise AuthenticationFailed('Необходимо ввести email и пароль.')
    #
    #     user = User.objects.filter(email=email).first()
    #
    #     if user is None or not user.check_password(password):
    #         raise AuthenticationFailed('Неверные учетные данные.')
    #
    #     # Получение стандартных токенов после успешной аутентификации
    #     refresh = self.get_token(user) # Используем переопределенный get_token
    #
    #     data = {
    #         'refresh': str(refresh),
    #         'access': str(refresh.access_token),
    #     }
    #
    #     return data

Затем укажите ваш кастомный сериализатор в настройках SIMPLE_JWT:

# settings.py

SIMPLE_JWT = {
    # ... другие настройки
    'TOKEN_OBTAIN_SERIALIZER': 'users.serializers.CustomTokenObtainPairSerializer', # Путь к вашему сериализатору
    # ...
}

Пример пользовательской реализации выдачи JWT токенов

Хотя переопределение сериализатора является стандартным способом кастомизации с simple-jwt, вы можете полностью создать собственное представление, если это необходимо. Однако, в большинстве случаев, использование стандартных views simple-jwt с кастомным сериализатором является более предпочтительным и менее трудозатратным подходом.

Реклама

Если же вы все-таки решите написать view с нуля (например, для нестандартного процесса аутентификации), вам потребуется вручную создать токены, используя классы из rest_framework_simplejwt.tokens:

# users/views.py (пример полностью кастомного view)

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from django.contrib.auth import authenticate
from rest_framework_simplejwt.tokens import RefreshToken
# from .serializers import CustomTokenObtainPairSerializer # Используйте свой сериализатор, если нужно

class CustomLoginView(APIView):
    permission_classes = ()
    authentication_classes = ()

    def post(self, request, *args, **kwargs):
        username = request.data.get("username")
        password = request.data.get("password")

        # Стандартная аутентификация Django
        user = authenticate(request, username=username, password=password)

        if user is not None:
            # Создание токенов для аутентифицированного пользователя
            refresh = RefreshToken.for_user(user)
            access = refresh.access_token

            # Добавление кастомных клеймов, если необходимо
            access['username'] = user.username
            # ... другие клеймы

            return Response({
                'access': str(access),
                'refresh': str(refresh),
            }, status=status.HTTP_200_OK)
        else:
            return Response({
                'detail': 'Неверные учетные данные.'
            }, status=status.HTTP_401_UNAUTHORIZED)

Затем привяжите это view к URL в urls.py:

# urls.py

from django.urls import path
from .views import CustomLoginView # Импортируйте ваше кастомное view

urlpatterns = [
    # ... другие URL'ы
    path('api/custom-login/', CustomLoginView.as_view(), name='custom_login'),
    # ...
]

Повторим, в большинстве случаев достаточно переопределить сериализатор TokenObtainPairSerializer вместо написания полностью кастомного view.

Использование JWT токена для аутентификации

После получения токена, клиент должен использовать его для доступа к защищенным ресурсам API.

Защита views с помощью JWTAuthentication

Как было показано в разделе настройки, установка JWTAuthentication в DEFAULT_AUTHENTICATION_CLASSES глобально защищает ваши views. Вы также можете применить его к конкретному view или ViewSet:

# app/views.py

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework_simplejwt.authentication import JWTAuthentication

class ProtectedView(APIView):
    # Явно указываем классы аутентификации и разрешений
    authentication_classes = (JWTAuthentication,)
    permission_classes = (IsAuthenticated,)

    def get(self, request):
        # Если запрос дошел до этой точки, пользователь аутентифицирован
        user = request.user # Объект пользователя доступен через request.user
        return Response({"message": f"Привет, {user.username}! Это защищенный ресурс."}) # type: ignore

Теперь только запросы с действительным JWT токеном в заголовке Authorization смогут получить доступ к этому view.

Отправка JWT токена в заголовке Authorization

Клиент (браузер, мобильное приложение, другой сервис) должен включать access токен в заголовок Authorization каждого запроса к защищенным endpoints. Формат заголовка:

Authorization: Bearer <ваш_access_токен>

Пример на JavaScript (используя Fetch API):

const accessToken = localStorage.getItem('accessToken'); // Или где вы храните токен

fetch('/api/protected-resource/', {
  method: 'GET',
  headers: {
    'Authorization': `Bearer ${accessToken}`,
    'Content-Type': 'application/json'
  }
})
.then(response => {
  if (!response.ok) {
    // Обработка ошибки (например, токен недействителен/истек)
    if (response.status === 401) {
       console.error('Неавторизованный доступ. Возможно, токен истек.');
       // Запросить новый access токен с помощью refresh токена
    }
    throw new Error(`HTTP error! status: ${response.status}`);
  }
  return response.json();
})
.then(data => {
  console.log(data);
})
.catch(error => {
  console.error('Ошибка при запросе:', error);
});

JWTAuthentication в DRF автоматически извлечет токен из этого заголовка, проверит его подпись и срок действия, и установит request.user и request.auth.

Обработка случаев истечения срока действия токена

Access токены обычно имеют короткий срок жизни для минимизации рисков безопасности в случае компрометации. Когда access токен истекает, запрос к защищенному ресурсу вернет ошибку 401 Unauthorized.

Клиент должен быть готов обработать эту ошибку. Типичный сценарий:

Клиент делает запрос с access токеном.

Получает ответ 401.

Клиент использует refresh токен (который имеет более долгий срок жизни) для запроса к /api/token/refresh/ endpoint.

Если refresh токен действителен, сервер выдает новую пару access и refresh токенов.

Клиент сохраняет новые токены и повторяет исходный запрос с новым access токеном.

Если и refresh токен истек, клиент должен предложить пользователю пройти аутентификацию заново (войти в систему).

Продвинутые темы и кастомизация

simple-jwt предоставляет гибкость для адаптации JWT аутентификации под специфические нужды вашего приложения.

Добавление пользовательских данных в JWT payload

Как было показано в примере кастомизации TokenObtainPairSerializer, вы можете добавить любые нечувствительные данные о пользователе в Payload access токена. Это делается путем добавления полей в объект токена перед его возвратом из метода get_token:

# ... в CustomTokenObtainPairSerializer.get_token

token['email'] = user.email # Добавляем email
token['is_staff'] = user.is_staff # Добавляем флаг staff
# ... любые другие поля модели пользователя или связанные данные

Эти данные будут доступны на клиенте после декодирования access токена (без проверки подписи — для получения данных) и на сервере через request.auth после успешной аутентификации (request.auth.payload).

Реализация ролей и прав доступа с использованием JWT

JWT аутентификация занимается кто делает запрос (request.user). Авторизация (права доступа, что пользователь может делать) реализуется отдельно с помощью DRF Permissions.

Вы можете комбинировать JWT с Permissions:

Используйте стандартные IsAuthenticated, IsAdminUser.

Создавайте свои кастомные классы разрешений, которые проверяют request.user (объект пользователя, полученный через JWT). Например, разрешение, проверяющее наличие определенной роли (если роль добавлена в Payload или получается из объекта пользователя).

Пример кастомного разрешения:

# app/permissions.py

from rest_framework.permissions import BasePermission
from django.contrib.auth.models import User # Или ваша кастомная модель пользователя

class IsManager(BasePermission):
    """
    Проверяет, является ли пользователь менеджером (пример).
    """
    def has_permission(self, request, view) -> bool:
        # Проверка, аутентифицирован ли пользователь и является ли он менеджером
        # Предполагается, что у модели User есть поле 'is_manager'
        return request.user and request.user.is_authenticated and getattr(request.user, 'is_manager', False)

Используйте его в view:

# app/views.py

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from .permissions import IsManager # Импортируем кастомное разрешение

class ManagerOnlyView(APIView):
    authentication_classes = (JWTAuthentication,)
    permission_classes = (IsAuthenticated, IsManager,) # Требуем и аутентификации, и быть менеджером

    def get(self, request):
        return Response({"message": "Привет, менеджер! Это ресурс только для менеджеров."})

JWT сам по себе не предоставляет механизмы авторизации, но он отлично работает в связке с системой разрешений DRF.

Обновление токенов: refresh tokens и механизмы их ротации

djangorestframework-simplejwt по умолчанию использует пару access/refresh токенов. Refresh токен используется исключительно для получения новой пары access/refresh токенов, когда access токен истек.

Вы можете включить ротацию refresh токенов, установив ROTATE_REFRESH_TOKENS: True в SIMPLE_JWT настройках. В этом случае при каждом использовании refresh токена для получения новой пары, будет выдан новый refresh токен, а старый может быть внесен в черный список (если BLACKLIST_AFTER_ROTATION: True), что повышает безопасность, делая старые refresh токены недействительными сразу после использования.

Безопасность JWT: best practices

HTTPS: Всегда используйте HTTPS для передачи токенов. Токены передаются в открытом виде (хотя Payload Base64-кодирован), и их перехват через HTTP может привести к компрометации.

Короткий срок жизни access токенов: Устанавливайте ACCESS_TOKEN_LIFETIME достаточно коротким (например, 5-15 минут). Это ограничивает время, в течение которого скомпрометированный токен может быть использован.

Безопасное хранение на клиенте: На стороне клиента избегайте хранения токенов (особенно refresh) в localStorage, так как он уязвим для XSS-атак. Предпочтительнее использовать HttpOnly cookies (с Secure флагом при работе по HTTPS), хотя это может усложнить работу с SPA.

Не храните чувствительные данные в Payload: Payload Base64-кодирован, но не зашифрован. Любые данные в Payload видны клиенту. Не включайте туда пароли, номера кредитных карт или другую конфиденциальную информацию.

Используйте Refresh токены: Разделение на access и refresh токены — важный паттерн безопасности. Access токены — короткоживущие и часто передаваемые. Refresh токены — долгоживущие, передаются редко и только для получения новых access токенов. Это минимизирует риски при утечке access токена.

Черные списки (Optional): simple-jwt поддерживает механизм черных списков для refresh токенов (BLACKLIST_AFTER_ROTATION=True). Это позволяет деактивировать токены до их истечения (например, при выходе пользователя). Для этого потребуется настройка базы данных и миграции для simple-jwt.

Следуя этим рекомендациям, вы сможете построить надежную систему аутентификации на основе JWT в вашем проекте на Django REST Framework.


Добавить комментарий