Шаг 08. JWT-аутентификация с SimpleJWT
⚡ Кратко: что делаем на этом шаге
Цель: Добавить JWT-аутентификацию через SimpleJWT. Эндпоинты: регистрация (POST /api/auth/register/), логин (POST /api/auth/token/), обновление токена (POST /api/auth/token/refresh/). Подключить JWTAuthentication глобально.
- Установка:
pip install djangorestframework-simplejwt - Файлы:
apps/users/serializers.py,apps/users/views.py,apps/users/urls.py,config/settings.py,config/urls.py - Проверка: POST
/api/auth/token/→ получить access+refresh токены
🎯 Цель этапа
До этого шага наш API открыт — кто угодно может читать и писать данные. На этом шаге добавляем JWT-аутентификацию: пользователь получает два токена — access (короткоживущий, для запросов) и refresh (долгоживущий, для обновления access).
- Пользователь регистрируется —
POST /api/auth/register/ - Пользователь логинится —
POST /api/auth/token/→ получаетaccess+refresh - Для защищённых эндпоинтов:
Authorization: Bearer <access_token> - Когда access истёк —
POST /api/auth/token/refresh/→ получает новый access
После этого шага у нас будет
- Регистрация нового пользователя через API
- Получение JWT access+refresh через логин
- Обновление access-токена через refresh
- Глобальная защита API через
JWTAuthentication
📄 Затрагиваемые файлы
| Файл | Действие | Описание |
|---|---|---|
apps/users/serializers.py | Обновить | RegisterSerializer с валидацией пароля |
apps/users/views.py | Создать/обновить | RegisterView (CreateAPIView) |
apps/users/urls.py | Создать/обновить | Маршруты: register, token, token/refresh |
config/settings.py | Обновить | SIMPLE_JWT настройки + DEFAULT_AUTHENTICATION_CLASSES |
config/urls.py | Обновить | Подключить /api/auth/ маршруты |
🔨 Шаги
1. Установка SimpleJWT
pip install djangorestframework-simplejwt
pip freeze | grep simplejwt >> requirements.txt
2. Настройки в settings.py
from datetime import timedelta
# Добавить в INSTALLED_APPS:
INSTALLED_APPS = [
# ...
"rest_framework",
"rest_framework_simplejwt", # ← добавить
"rest_framework_simplejwt.token_blacklist", # ← для blacklist при ротации (+ manage.py migrate)
"django_filters",
# ...
]
REST_FRAMEWORK = {
# Глобальный класс аутентификации — JWT
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework_simplejwt.authentication.JWTAuthentication",
],
# Глобальный класс разрешений — требуем аутентификацию по умолчанию
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
"DEFAULT_FILTER_BACKENDS": [
"django_filters.rest_framework.DjangoFilterBackend",
"rest_framework.filters.SearchFilter",
"rest_framework.filters.OrderingFilter",
],
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 20,
}
# Конфигурация SimpleJWT
SIMPLE_JWT = {
# Время жизни access-токена — 15 минут (рекомендация для безопасности)
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=15),
# Время жизни refresh-токена — 7 дней
"REFRESH_TOKEN_LIFETIME": timedelta(days=7),
# Ротация refresh-токенов: при обновлении выдаётся новый refresh
"ROTATE_REFRESH_TOKENS": True,
# Занести старый refresh в blacklist, чтобы он реально стал недействительным.
# Требует app "rest_framework_simplejwt.token_blacklist" + python manage.py migrate
"BLACKLIST_AFTER_ROTATION": True,
# Алгоритм подписи
"ALGORITHM": "HS256",
# Поле из User-модели, которое кладётся в payload как user_id
"USER_ID_FIELD": "id",
"USER_ID_CLAIM": "user_id",
# Добавить в payload email — для отладки и клиентской идентификации
"TOKEN_OBTAIN_SERIALIZER": "apps.users.serializers.CustomTokenObtainPairSerializer",
}
3. Кастомный TokenObtainPairSerializer — добавляем email в payload
# apps/users/serializers.py
from django.contrib.auth import get_user_model
from rest_framework import serializers
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
User = get_user_model()
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
"""
Расширяем стандартный payload SimpleJWT.
По умолчанию payload содержит: token_type, exp, iat, jti, user_id.
Добавляем: email, username — для удобства клиента (не надо делать GET /users/me/).
"""
@classmethod
def get_token(cls, user):
token = super().get_token(user)
# Дополнительные поля в payload
token["email"] = user.email
token["username"] = user.username
return token
class RegisterSerializer(serializers.ModelSerializer):
"""
Сериализатор регистрации пользователя.
Поля: email, username, password, password_confirm, bio (опционально).
"""
password = serializers.CharField(
write_only=True,
min_length=8,
style={"input_type": "password"},
)
password_confirm = serializers.CharField(
write_only=True,
style={"input_type": "password"},
)
class Meta:
model = User
fields = ["email", "username", "password", "password_confirm", "bio"]
extra_kwargs = {
"bio": {"required": False, "allow_blank": True},
}
def validate(self, attrs):
if attrs["password"] != attrs["password_confirm"]:
raise serializers.ValidationError(
{"password_confirm": "Пароли не совпадают."}
)
return attrs
def create(self, validated_data):
# Убираем password_confirm — его нет в модели
validated_data.pop("password_confirm")
password = validated_data.pop("password")
user = User(**validated_data)
# Хешируем пароль через встроенный метод Django
user.set_password(password)
user.save()
return user
class UserProfileSerializer(serializers.ModelSerializer):
"""Профиль пользователя — используется в /api/users/me/."""
class Meta:
model = User
fields = ["id", "email", "username", "bio", "avatar", "date_joined"]
read_only_fields = ["id", "email", "date_joined"]
4. View для регистрации
# apps/users/views.py
from django.contrib.auth import get_user_model
from rest_framework import generics, permissions
from rest_framework.response import Response
from rest_framework.views import APIView
from .serializers import RegisterSerializer, UserProfileSerializer
User = get_user_model()
class RegisterView(generics.CreateAPIView):
"""
POST /api/auth/register/
Регистрация нового пользователя. Открытый эндпоинт — аутентификация не нужна.
"""
queryset = User.objects.all()
serializer_class = RegisterSerializer
permission_classes = [permissions.AllowAny]
class MeView(APIView):
"""
GET /api/auth/me/ — получить профиль текущего пользователя
PATCH /api/auth/me/ — обновить bio/avatar
"""
permission_classes = [permissions.IsAuthenticated]
def get(self, request):
serializer = UserProfileSerializer(request.user)
return Response(serializer.data)
def patch(self, request):
serializer = UserProfileSerializer(
request.user, data=request.data, partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
5. URL-маршруты пользователей
# apps/users/urls.py
from django.urls import path
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
TokenVerifyView,
)
from .views import RegisterView, MeView
urlpatterns = [
# Регистрация нового пользователя
path("register/", RegisterView.as_view(), name="auth-register"),
# Логин: POST {email, password} → {access, refresh}
path("token/", TokenObtainPairView.as_view(), name="token-obtain"),
# Обновление access-токена: POST {refresh} → {access}
path("token/refresh/", TokenRefreshView.as_view(), name="token-refresh"),
# Верификация токена: POST {token} → 200 OK или 401
path("token/verify/", TokenVerifyView.as_view(), name="token-verify"),
# Профиль текущего пользователя
path("me/", MeView.as_view(), name="auth-me"),
]
6. Подключение в корневых URL
# config/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
# Auth: регистрация, логин, refresh, me
path("api/auth/", include("apps.users.urls")),
# API ресурсов
path("api/", include("apps.projects.urls")),
path("api/", include("apps.tasks.urls")),
# Swagger — добавим на шаге 12
]
AUTH_COOKIE-настройки или пакет
djangorestframework-simplejwt[cookie].
Для учебного проекта JSON-ответ достаточен; для production-систем обязательно реализуйте httpOnly-куки и CSRF-защиту.
🧠 Объяснение логики
Структура JWT
JWT состоит из трёх частей, разделённых точкой: header.payload.signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ← header (base64)
.eyJ1c2VyX2lkIjoxLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIn0= ← payload (base64)
.HMACSHA256(header + payload, SECRET_KEY) ← подпись
Сервер НЕ хранит токены — он только проверяет подпись. Это делает JWT масштабируемым: любой узел кластера может валидировать токен без обращения к БД.
Access vs Refresh — почему два токена
| Параметр | Access token | Refresh token |
|---|---|---|
| Время жизни | 15 минут | 7 дней |
| Использование | В каждом запросе | Только для получения нового access |
| Хранение (rec.) | Память / httpOnly cookie | httpOnly cookie |
| При компрометации | Риск минимален (15 мин) | Нужна инвалидация (blacklist) |
USERNAME_FIELD = "email" — нюанс SimpleJWT
Наш User использует email как USERNAME_FIELD.
SimpleJWT по умолчанию использует поле username для логина.
TokenObtainPairSerializer автоматически использует USERNAME_FIELD
модели — поэтому в теле запроса нужно передавать email, а не username.
ROTATE_REFRESH_TOKENS + BLACKLIST_AFTER_ROTATION
При каждом обновлении выдаётся новый refresh-токен (ROTATE_REFRESH_TOKENS=True),
а старый заносится в blacklist (BLACKLIST_AFTER_ROTATION=True) и становится
действительно недействительным. Без blacklist старый refresh оставался бы
валидным до истечения срока — поэтому оба флага работают в паре.
Это усиливает безопасность: если злоумышленник похитил refresh-токен, то после первого
обновления легитимным пользователем украденный токен перестанет работать.
rest_framework_simplejwt.token_blacklist
в INSTALLED_APPS и python manage.py migrate.
✅ Проверка
1. Регистрация пользователя
curl -s -X POST http://127.0.0.1:8000/api/auth/register/ \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"username": "testuser",
"password": "Str0ngPass!",
"password_confirm": "Str0ngPass!"
}' | python -m json.tool
{
"id": 2,
"email": "user@example.com",
"username": "testuser",
"bio": ""
}
2. Получение JWT-токенов (логин)
curl -s -X POST http://127.0.0.1:8000/api/auth/token/ \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "password": "Str0ngPass!"}' \
| python -m json.tool
{
"access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
}
3. Запрос к защищённому эндпоинту с токеном
# Сохраняем access-токен в переменную
ACCESS=$(curl -s -X POST http://127.0.0.1:8000/api/auth/token/ \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "password": "Str0ngPass!"}' \
| python -c "import sys,json; print(json.load(sys.stdin)['access'])")
# Запрос с Bearer-токеном
curl -s http://127.0.0.1:8000/api/tasks/ \
-H "Authorization: Bearer $ACCESS" \
| python -m json.tool
4. Обновление access-токена
REFRESH="ваш_refresh_токен_здесь"
curl -s -X POST http://127.0.0.1:8000/api/auth/token/refresh/ \
-H "Content-Type: application/json" \
-d "{\"refresh\": \"$REFRESH\"}" \
| python -m json.tool
5. Проверка профиля
curl -s http://127.0.0.1:8000/api/auth/me/ \
-H "Authorization: Bearer $ACCESS" \
| python -m json.tool
Диагностика ошибок
- 401 при логине — убедитесь что передаёте
email, а неusername(USERNAME_FIELD = "email") detail: No active account found— пользователь не создан или email неверный- 401 при обращении к API — токен истёк (15 минут), обновите через
/api/auth/token/refresh/ ImportError: CustomTokenObtainPairSerializer— проверьте путь вSIMPLE_JWT["TOKEN_OBTAIN_SERIALIZER"]
➡️ Что дальше
JWT-аутентификация настроена. Теперь каждый запрос к API требует токена. Следующий шаг — разграничение прав: не каждый аутентифицированный пользователь должен мочь редактировать любой объект. Нужны permissions на уровне объекта.
- Готово: регистрация, JWT login/refresh, защита всех эндпоинтов
- Шаг 09: кастомные permissions — IsProjectOwner, IsProjectMember, IsTaskAssignee