При разработке современных веб-приложений и мобильных сервисов, 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. Это позволяет