Как реализовать JWT-аутентификацию с использованием Django REST Framework: Пошаговое руководство

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

Что такое JWT (JSON Web Token) и как он работает?

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

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

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

Подпись (Signature): Создается путем кодирования заголовка и полезной нагрузки в Base64Url и их подписи с использованием секретного ключа сервера и алгоритма, указанного в заголовке.

Когда клиент получает JWT после успешной аутентификации, он хранит его (например, в Local Storage или куках). При каждом последующем запросе к защищенным ресурсам клиент отправляет этот токен (обычно в заголовке Authorization с префиксом Bearer). Сервер получает токен, верифицирует его подпись с помощью того же секретного ключа, чтобы убедиться в его целостности и подлинности, а затем использует данные из полезной нагрузки для идентификации пользователя и проверки его прав доступа. Ключевое преимущество JWT в его безгосударственности (stateless): серверу не нужно хранить информацию о сессии каждого пользователя.

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

Использование JWT для аутентификации в REST API дает несколько значительных преимуществ:

Безгосударственность (Stateless): Серверу не нужно хранить состояние сессии пользователя, что упрощает масштабирование API, особенно в распределенных системах и микросервисной архитектуре. Каждый запрос содержит всю необходимую информацию.

Производительность: Проверка подписи токена обычно быстрее, чем запрос к базе данных для проверки состояния сессии.

Мобильность: Токен может быть легко передан между различными сервисами или приложениями, которые используют один и тот же секретный ключ для проверки.

Безопасность: Подпись гарантирует, что токен не был изменен после выдачи. Использование HTTPS обязательно для предотвращения перехвата токенов.

Поддержка CORS: JWT упрощает работу с Cross-Origin Resource Sharing (CORS), поскольку токены передаются в заголовках, а не как специфичные для домена куки.

Обзор Django REST Framework и его возможностей для аутентификации

Django REST Framework (DRF) – это мощный и гибкий набор инструментов для построения веб-API на Django. DRF предоставляет множество компонентов для быстрой разработки API, включая сериализаторы для преобразования данных, классы представлений (Views) для обработки запросов и, что важно для нас, гибкую систему аутентификации и разрешений.

DRF поддерживает различные схемы аутентификации из коробки (например, Basic Authentication, Session Authentication). Однако для современных stateless API, таких как те, что взаимодействуют с мобильными приложениями или SPA (Single Page Applications), JWT является предпочтительным выбором. Хотя DRF не включает встроенную поддержку JWT, он предоставляет абстрактные классы Authentication и Permission, которые позволяют легко интегрировать сторонние библиотеки, такие как djangorestframework-simplejwt, для работы с JWT.

Настройка Django-проекта и установка необходимых пакетов

Прежде чем приступить к реализации, нам необходимо подготовить Django-проект.

Создание нового Django-проекта и приложения

Предполагается, что у вас установлен Python и Django. Создайте новый проект и приложение:

# Создание виртуального окружения (рекомендуется)
python -m venv venv
source venv/bin/activate  # для Linux/macOS
# venv\Scripts\activate   # для Windows

# Установка Django
pip install Django

# Создание проекта
django-admin startproject myjwtproject .

# Создание приложения для API
python manage.py startapp api

Не забудьте добавить api в список INSTALLED_APPS в settings.py вашего проекта.

Установка Django REST Framework

Установите DRF с помощью pip:

pip install djangorestframework

Добавьте rest_framework в список INSTALLED_APPS в settings.py.

Установка пакета Simple JWT для работы с JWT

djangorestframework-simplejwt – это популярная и хорошо поддерживаемая библиотека для реализации JWT-аутентификации в DRF.

pip install djangorestframework-simplejwt

Добавьте rest_framework_simplejwt в список INSTALLED_APPS в settings.py.

Настройка settings.py для использования DRF и Simple JWT

Необходимо указать DRF использовать JWT в качестве схемы аутентификации по умолчанию и настроить Simple JWT.

Откройте файл settings.py и добавьте/измените следующие настройки:

# settings.py

INSTALLED_APPS = [
    ...
    'rest_framework',
    'rest_framework_simplejwt',
    'api',
    ...
]

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
    # Можете добавить другие настройки DRF здесь, например,
    # 'DEFAULT_PERMISSION_CLASSES': [
    #     'rest_framework.permissions.IsAuthenticated',
    # ]
}

from datetime import timedelta

SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5), # Время жизни Access Token
    'REFRESH_TOKEN_LIFETIME': timedelta(days=1),  # Время жизни Refresh Token
    'ROTATE_REFRESH_TOKENS': False,              # Менять Refresh Token при каждом обновлении
    'BLACKLIST_AFTER_ROTATION': False,           # Заносить старый Refresh Token в черный список
    'UPDATE_LAST_LOGIN': False,                  # Обновлять дату последнего входа пользователя при аутентификации

    '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),
}

# Добавьте путь для получения токенов в urls.py вашего проекта
# project/urls.py
# from rest_framework_simplejwt.views import (
#     TokenObtainPairView,
#     TokenRefreshView,
# )
#
# urlpatterns = [
#     ...
#     path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
#     path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
# ]

Не забудьте добавить URL-пути Simple JWT в ваш главный urls.py файл, как показано в закомментированном примере выше. Это предоставит эндпоинты /api/token/ для получения access и refresh токенов по паре логин/пароль и /api/token/refresh/ для обновления access токена с использованием refresh токена.

Теперь выполните миграции, чтобы создать необходимые модели пользователей Django (если вы еще этого не сделали): python manage.py migrate.

Реализация JWT-аутентификации с использованием Simple JWT

Теперь, когда проект настроен, перейдем к реализации API-эндпоинтов.

Создание сериализаторов для регистрации и аутентификации пользователей

Для регистрации пользователей нам потребуется сериализатор, который будет валидировать данные и создавать пользователя. Для аутентификации мы воспользуемся стандартными сериализаторами Simple JWT.

Создайте файл api/serializers.py:

# api/serializers.py
from rest_framework import serializers
from django.contrib.auth.models import User

class UserSerializer(serializers.ModelSerializer):
    password = serializers.CharField(write_only=True)

    class Meta:
        model = User
        fields = ['id', 'username', 'password', 'email']

    def create(self, validated_data: dict) -> User:
        """Создает нового пользователя с захешированным паролем."""
        user = User.objects.create_user(
            username=validated_data['username'],
            password=validated_data['password'],
            email=validated_data.get('email', '') # email опционален
        )
        return user

Сериализатор UserSerializer обрабатывает создание нового пользователя. Поле password помечено как write_only, чтобы оно не возвращалось в ответе API. Метод create переопределен для корректного создания пользователя с использованием create_user, который автоматически хеширует пароль.

Simple JWT предоставляет готовые сериализаторы для получения и обновления токенов: TokenObtainPairSerializer и TokenRefreshSerializer. Их не нужно явно создавать в вашем приложении, они используются в представлениях Simple JWT (TokenObtainPairView, TokenRefreshView), которые мы подключили в urls.py.

Настройка представлений (views) для регистрации, получения токена и обновления токена

Нам потребуется представление для регистрации новых пользователей. Представления для получения и обновления токенов уже предоставлены Simple JWT.

Создайте или отредактируйте файл api/views.py:

Реклама
# api/views.py
from rest_framework import generics, status
from rest_framework.response import Response
from rest_framework.permissions import AllowAny # Разрешить доступ без аутентификации
from .serializers import UserSerializer

class RegisterView(generics.CreateAPIView):
    """API представление для регистрации новых пользователей."""
    queryset = User.objects.all()
    permission_classes = (AllowAny,)
    serializer_class = UserSerializer

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        user = serializer.save()
        headers = self.get_success_headers(serializer.data)
        # Вместо полного ответа с данными пользователя, можно вернуть только статус
        return Response({
            "message": "User registered successfully.",
            "user_id": user.id,
            "username": user.username
        }, status=status.HTTP_201_CREATED, headers=headers)

# Представления для получения и обновления токенов предоставляются Simple JWT:
# rest_framework_simplejwt.views.TokenObtainPairView (POST для логина)
# rest_framework_simplejwt.views.TokenRefreshView (POST для обновления)

Теперь необходимо привязать это представление к URL-пути. Отредактируйте файл api/urls.py (создайте его, если не существует):

# api/urls.py
from django.urls import path
from .views import RegisterView
from rest_framework_simplejwt.views import (
    TokenObtainPairView,
    TokenRefreshView,
    TokenVerifyView, # Дополнительно: для проверки валидности токена без аутентификации
)

urlpatterns = [
    path('register/', RegisterView.as_view(), name='auth_register'),
    path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
    path('token/verify/', TokenVerifyView.as_view(), name='token_verify'),
]

Включите URL-ы приложения api в главный urls.py проекта:

# project/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('api.urls')), # Включаем URL-ы нашего API приложения
]

Теперь у вас есть эндпоинт /api/register/ для регистрации, /api/token/ для получения JWT по логину/паролю и /api/token/refresh/ для обновления access токена.

Защита эндпоинтов API с помощью JWT-аутентификации

Чтобы защитить определенные эндпоинты и требовать наличия действительного JWT, используйте классы разрешений DRF. Наиболее распространенный – IsAuthenticated, который требует, чтобы пользователь был аутентифицирован.

Пример защищенного представления:

# api/views.py
from rest_framework import generics
from rest_framework.permissions import IsAuthenticated # Требовать аутентификации
from rest_framework.response import Response
from django.contrib.auth.models import User

class ProtectedView(generics.RetrieveAPIView):
    """Пример защищенного представления, требующего JWT."""
    permission_classes = (IsAuthenticated,)
    queryset = User.objects.all()
    serializer_class = UserSerializer # Пример использования сериализатора пользователя

    # В реальном приложении здесь была бы логика получения данных,
    # доступных только аутентифицированным пользователям
    def retrieve(self, request, *args, **kwargs):
        # request.user будет объектом пользователя, если токен валиден
        user = request.user
        return Response({
            "message": "This is a protected endpoint",
            "user_id": user.id,
            "username": user.username,
            "is_authenticated": user.is_authenticated
        })

Добавьте соответствующий URL-путь для этого представления в api/urls.py:

# api/urls.py
# ... импорты ...
from .views import RegisterView, ProtectedView
# ... импорты Simple JWT views ...

urlpatterns = [
    path('register/', RegisterView.as_view(), name='auth_register'),
    path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
    path('token/verify/', TokenVerifyView.as_view(), name='token_verify'),
    path('protected/', ProtectedView.as_view(), name='protected_resource'), # Новый защищенный путь
]

Теперь при обращении к /api/protected/ без валидного JWT в заголовке Authorization: Bearer <token>, вы получите ошибку 401 Unauthorized. С валидным токеном вы получите ответ от представления.

Кастомизация JWT: добавление пользовательских данных в токен

Иногда полезно включить дополнительные данные в полезную нагрузку токена, чтобы избежать лишних запросов к базе данных при каждом запросе. Simple JWT позволяет легко это сделать, переопределив метод get_token у сериализатора TokenObtainPairSerializer.

Создайте новый сериализатор, наследующий от TokenObtainPairSerializer:

# api/serializers.py
# ... импорты ...
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer

class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
    @classmethod
    def get_token(cls, user):
        """Переопределяем для добавления пользовательских данных в токен."""
        token = super().get_token(user)

        # Добавление пользовательских утверждений (claims)
        token['username'] = user.username
        token['email'] = user.email
        # Пример: добавление флага суперпользователя
        token['is_superuser'] = user.is_superuser

        return token

Теперь необходимо указать Simple JWT использовать этот кастомный сериализатор вместо стандартного. Сделайте это в settings.py:

# settings.py
# ... настройки SIMPLE_JWT ...

SIMPLE_JWT = {
    ...
    'TOKEN_OBTAIN_SERIALIZER': 'api.serializers.CustomTokenObtainPairSerializer',
    ...
}

После этой настройки, при получении токена через эндпоинт /api/token/, полезная нагрузка access токена будет содержать дополнительные поля username, email и is_superuser.

Важно: Не добавляйте в токен чувствительные данные или слишком много информации, так как токен передается с каждым запросом. Также помните, что данные в полезной нагрузке не шифруются, они только кодируются и подписываются. Любой может прочитать содержимое полезной нагрузки, но не изменить его незаметно.

Работа с Refresh Token и безопасностью JWT

Использование только долгоживущих Access Tokens представляет риск, если токен будет скомпрометирован. Refresh Tokens решают эту проблему.

Реализация обновления JWT с использованием Refresh Token

Simple JWT автоматически предоставляет механизм Refresh Tokens. Когда клиент аутентифицируется, он получает как Access Token (короткоживущий), так и Refresh Token (долгоживущий). Когда Access Token истекает, клиент отправляет Refresh Token на специальный эндпоинт (/api/token/refresh/), и Simple JWT, если Refresh Token действителен, выдает новую пару Access и Refresh токенов (в зависимости от настроек).

Эндпоинт /api/token/refresh/ мы уже подключили в urls.py. Он использует стандартное представление TokenRefreshView и сериализатор TokenRefreshSerializer из Simple JWT.

Клиентская логика должна быть следующей:

При логине получить пару токенов (Access и Refresh).

Хранить оба токена.

При каждом запросе к защищенному ресурсу отправлять Access Token.

Если запрос возвращает ошибку 401 Unauthorized (из-за истекшего Access Token), использовать Refresh Token для получения новой пары токенов с эндпоинта /api/token/refresh/.

Повторить исходный запрос с новым Access Token.

Если обновление токена с помощью Refresh Token не удалось (например, Refresh Token истек или был отозван), выполнить повторную аутентификацию (логин).

Настройка времени жизни токенов (access и refresh)

Время жизни токенов настраивается в словаре SIMPLE_JWT в settings.py с помощью ключей ACCESS_TOKEN_LIFETIME и REFRESH_TOKEN_LIFETIME. Хорошей практикой является установка короткого времени жизни для Access Token (например, 5-15 минут) и более длительного для Refresh Token (например, дни или недели).

# settings.py
from datetime import timedelta

SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=10), # Сделаем 10 минут
    'REFRESH_TOKEN_LIFETIME': timedelta(days=7),   # Сделаем 7 дней
    # ... остальные настройки ...
}

Эти настройки влияют на то, как часто клиенту придется обновлять Access Token.

Рекомендации по безопасности JWT: хранение токенов, защита от XSS и CSRF

Безопасность при работе с JWT зависит как от реализации на сервере, так и от правильного хранения и использования токенов на клиенте.

HTTPS: Всегда используйте HTTPS для всех API-запросов, чтобы предотвратить перехват токенов в процессе передачи.

Хранение токенов на клиенте:

Local Storage: Просто в использовании, но уязвим для XSS-атак. Злоумышленник, внедривший вредоносный скрипт на вашу страницу, может получить доступ ко всем данным в Local Storage, включая токены.

HTTP-only Cookies: Куки с флагом HttpOnly недоступны из клиентского JavaScript, что защищает от XSS. Однако они уязвимы для CSRF-атак, если только сервер не реализует дополнительные меры (например, проверку заголовка Referer или использование дополнительных CSRF-токенов для не-GET запросов, хотя для stateless API это менее применимо, так как аутентификация происходит по заголовку Authorization, а не по кукам сессии). Для JWT, передаваемого в заголовке Authorization, основная уязвимость при хранении в HTTP-only куках — это возможность CSRF-атаки, которая заставит браузер отправить куку (с refresh токеном) на вредоносный сайт или сайт, который злоумышленник контролирует, чтобы получить новую пару токенов.

В памяти (In-memory): Самый безопасный способ с точки зрения XSS, но токен теряется при обновлении страницы или закрытии браузера. Обычно не подходит для долгосрочной аутентификации.

Рекомендация: Единого идеального способа нет. Часто выбирают HTTP-only куки для Refresh Token (который используется реже и менее критичен при утечке) и Local Storage или память для Access Token (который используется часто). Важно комбинировать меры: использовать надежную политику безопасности контента (CSP), регулярно обновлять зависимости, проводить аудиты безопасности кода.

Время жизни токенов: Используйте короткое время жизни для Access Token, чтобы уменьшить окно атаки в случае его компрометации. Rely on Refresh Tokens for long-term sessions.

Отзыв токенов (Token Blacklisting): Simple JWT поддерживает механизм черных списков для Refresh Tokens. Это позволяет


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