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.