🏠 Домашнее задание 20 — Урок 44
⚡ Что нужно сделать
- Задание 1: Регистрация — валидация полей, уникальность email/логина, сложность пароля, хэширование
- Задание 2: Логин — вернуть JWT access + refresh токены в httpOnly-куках при успехе
- Задание 3: Логаут — поместить refresh-токен в blacklist и удалить оба токена из куки
Текст задания из LMS (ДЗ 20)
Домашнее задание: Работа с логином и регистрацией
Задание 1: Регистрация пользователя
Реализовать механизм регистрации пользователя в системе, учесть:
- Обязательная валидация полей (проверка наличия, формат, уникальность email/логина).
- Реализация минимальных требований к сложности пароля.
- Хэширование и сохранение пароля в БД.
Задание 2: Вход в аккаунт
Реализовать механизм входа в аккаунт. Проверять правильность вводимых данных и наличие пользователя. Если пользователь присутствует и данные входа валидны — возвращать JWT access и refresh токены.
В функционале должно быть учтено:
- Проверка корректности вводимых данных и существования пользователя.
- При успешной аутентификации — возвращение JWT access и refresh токенов.
- Безопасное хранение токенов на клиенте (httpOnly cookies) и механизм обновления access токена через refresh токен, с возможностью аннулирования.
Задание 3: Выход из аккаунта
Реализовать механизм выхода из аккаунта. При выходе из текущего аккаунта токены должны помещаться в blacklist и удаляться.
Подготовка окружения
1. Создание проекта
# Создать директорию и войти в неё
mkdir hw20_jwt_auth
cd hw20_jwt_auth
# Создать и активировать виртуальное окружение
python -m venv venv
venv\Scripts\activate # Windows
# source venv/bin/activate # macOS/Linux
# Установить зависимости
pip install django djangorestframework djangorestframework-simplejwt
# Зафиксировать зависимости
pip freeze > requirements.txt
2. Создание Django-проекта и приложения
django-admin startproject core .
python manage.py startapp accounts
3. Инициализация git
git init
# Создать .gitignore
echo "venv/
*.pyc
__pycache__/
db.sqlite3
.env" > .gitignore
git add .
git commit -m "initial: project scaffold"
Пошаговое решение
Шаг 1: Настройка settings.py
Связь с теорией: Теория → Часть 1: Настройка Simple JWT
# core/settings.py
from pathlib import Path
from datetime import timedelta
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = 'your-secret-key-change-in-production'
DEBUG = True
ALLOWED_HOSTS = ['localhost', '127.0.0.1']
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'rest_framework_simplejwt',
'rest_framework_simplejwt.token_blacklist', # Для задания 3
'accounts',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'core.urls'
TEMPLATES = [{'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [], 'APP_DIRS': True,
'OPTIONS': {'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
]}}]
WSGI_APPLICATION = 'core.wsgi.application'
DATABASES = {'default': {'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3'}}
AUTH_PASSWORD_VALIDATORS = [
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'},
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
]
LANGUAGE_CODE = 'ru-ru'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
STATIC_URL = '/static/'
# DRF: глобальная аутентификация через JWT
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
}
# SimpleJWT: настройки токенов
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
'AUTH_HEADER_TYPES': ('Bearer',),
}
Шаг 2: Применить миграции
python manage.py migrate
⚠️ Убедитесь, что
rest_framework_simplejwt.token_blacklist создал таблицы — при следующем запросе проверьте, что команда отработала без ошибок.
Шаг 3: serializers.py — валидация регистрации (Задание 1)
Связь с теорией: Теория → Часть 6: RegisterSerializer
# accounts/serializers.py
from rest_framework import serializers
from django.contrib.auth.models import User
from django.contrib.auth.password_validation import validate_password
from rest_framework.validators import UniqueValidator
class RegisterSerializer(serializers.ModelSerializer):
"""
Сериализатор для регистрации.
Задание 1: валидация полей, уникальность, сложность пароля, хэширование.
"""
email = serializers.EmailField(
required=True,
validators=[UniqueValidator(queryset=User.objects.all(),
message="Пользователь с таким email уже существует.")]
)
password = serializers.CharField(
write_only=True,
required=True,
validators=[validate_password] # MinimumLength, Common, Numeric, Similarity
)
password2 = serializers.CharField(write_only=True, required=True)
class Meta:
model = User
fields = ['username', 'email', 'password', 'password2']
extra_kwargs = {
'username': {
'validators': [
UniqueValidator(queryset=User.objects.all(),
message="Пользователь с таким именем уже существует.")
]
}
}
def validate(self, data):
"""Проверка совпадения паролей."""
if data['password'] != data['password2']:
raise serializers.ValidationError(
{"password": "Пароли не совпадают."}
)
return data
def create(self, validated_data):
"""
Создание пользователя.
create_user() автоматически хэширует пароль через set_password().
"""
validated_data.pop('password2') # Удаляем подтверждение пароля
user = User.objects.create_user(
username=validated_data['username'],
email=validated_data['email'],
password=validated_data['password']
)
return user
Шаг 4: views.py — LoginView, RegisterView, LogoutView
Связь с теорией: LoginView | RegisterView | LogoutView
# accounts/views.py
from datetime import datetime, timezone
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 rest_framework_simplejwt.exceptions import TokenError
from rest_framework.permissions import AllowAny
from .serializers import RegisterSerializer
def set_jwt_cookies(response, user):
"""Установить JWT-токены в httpOnly-куки ответа."""
refresh = RefreshToken.for_user(user)
access_token = refresh.access_token
access_expiry = datetime.fromtimestamp(access_token['exp'], tz=timezone.utc)
refresh_expiry = datetime.fromtimestamp(refresh['exp'], tz=timezone.utc)
response.set_cookie(
key='access_token',
value=str(access_token),
httponly=True,
secure=False, # True в продакшне
samesite='Lax',
expires=access_expiry
)
response.set_cookie(
key='refresh_token',
value=str(refresh),
httponly=True,
secure=False,
samesite='Lax',
expires=refresh_expiry
)
return refresh # Возвращаем refresh для возможного использования
class RegisterView(APIView):
"""
Задание 1: Регистрация пользователя.
Валидирует данные, хэширует пароль, возвращает JWT-токены.
"""
permission_classes = [AllowAny]
def post(self, request):
serializer = RegisterSerializer(data=request.data)
if serializer.is_valid():
user = serializer.save()
response = Response({
'message': 'Регистрация успешна.',
'user': {
'id': user.id,
'username': user.username,
'email': user.email
}
}, status=status.HTTP_201_CREATED)
set_jwt_cookies(response, user)
return response
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class LoginView(APIView):
"""
Задание 2: Вход в аккаунт.
Проверяет учётные данные, возвращает JWT access + refresh в httpOnly-куках.
"""
permission_classes = [AllowAny]
def post(self, request, *args, **kwargs):
username = request.data.get('username')
password = request.data.get('password')
if not username or not password:
return Response(
{"detail": "Необходимо указать username и password."},
status=status.HTTP_400_BAD_REQUEST
)
user = authenticate(request, username=username, password=password)
if user:
response = Response(
{"message": f"Добро пожаловать, {user.username}!"},
status=status.HTTP_200_OK
)
set_jwt_cookies(response, user)
return response
else:
return Response(
{"detail": "Неверный логин или пароль."},
status=status.HTTP_401_UNAUTHORIZED
)
class LogoutView(APIView):
"""
Задание 3: Выход из аккаунта.
Помещает refresh-токен в blacklist и удаляет оба токена из куки.
"""
def post(self, request, *args, **kwargs):
refresh_token_str = request.COOKIES.get('refresh_token')
if refresh_token_str:
try:
refresh = RefreshToken(refresh_token_str)
refresh.blacklist() # Аннулируем refresh-токен
except TokenError:
# Токен уже истёк или невалиден — нечего инвалидировать
pass
response = Response(
{"message": "Выход выполнен успешно."},
status=status.HTTP_200_OK
)
response.delete_cookie('access_token')
response.delete_cookie('refresh_token')
return response
class ProfileView(APIView):
"""Пример защищённого эндпоинта для проверки аутентификации."""
def get(self, request):
return Response({
'id': request.user.id,
'username': request.user.username,
'email': request.user.email,
'is_staff': request.user.is_staff,
})
Шаг 5: urls.py
# accounts/urls.py
from django.urls import path
from .views import RegisterView, LoginView, LogoutView, ProfileView
urlpatterns = [
path('register/', RegisterView.as_view(), name='register'),
path('login/', LoginView.as_view(), name='login'),
path('logout/', LogoutView.as_view(), name='logout'),
path('profile/', ProfileView.as_view(), name='profile'),
]
# core/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/auth/', include('accounts.urls')),
]
Шаг 6: Запустить сервер
python manage.py runserver
Проверка в VS Code
Запуск через терминал VS Code
- Открыть папку проекта:
File → Open Folder → hw20_jwt_auth - Открыть терминал:
Ctrl+`(backtick) - Активировать venv и запустить сервер:
venv\Scripts\activate python manage.py runserver - Сервер запущен:
http://127.0.0.1:8000/
Запуск через F5 (launch.json)
Создать файл .vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"name": "Django Server",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/manage.py",
"args": ["runserver", "8000"],
"django": true,
"env": {
"PYTHONPATH": "${workspaceFolder}"
},
"console": "integratedTerminal",
"justMyCode": true
}
]
}
После этого нажмите F5 или Run → Start Debugging.
Точки останова (breakpoints)
Для отладки views:
- Откройте
accounts/views.py - Кликните на поле слева от номера строки в
LoginView.post, например, на строке сuser = authenticate(...) - Появится красная точка — это breakpoint
- Запустите через F5, отправьте POST-запрос
- VS Code остановится на точке; в панели Variables видны все переменные (username, password, user)
- Используйте F10 (Step Over) для пошагового выполнения
Тестирование в Postman
Подготовка: создать пользователя-администратора
python manage.py createsuperuser
# Введите username, email, password при запросе
Тест 1: Регистрация нового пользователя
- Метод: POST
- URL:
http://127.0.0.1:8000/api/auth/register/ - Headers:
Content-Type: application/json - Body (JSON):
{ "username": "testuser", "email": "test@example.com", "password": "SecurePass123!", "password2": "SecurePass123!" } - Ожидаемый ответ: 201 Created, JSON с данными пользователя, Set-Cookie в заголовках
Тест 2: Регистрация с неверными данными (валидация)
- Слабый пароль:
"password": "123"→ 400 Bad Request, ошибки валидации - Несовпадение паролей: password ≠ password2 → 400 Bad Request
- Дублирующий email → 400 Bad Request с описанием ошибки
Тест 3: Вход в аккаунт
- Метод: POST
- URL:
http://127.0.0.1:8000/api/auth/login/ - Body (JSON):
{ "username": "testuser", "password": "SecurePass123!" } - Ожидаемый ответ: 200 OK с приветствием, Set-Cookie: access_token + refresh_token
Тест 4: Неверные учётные данные
- Неверный пароль → 401 Unauthorized
- Несуществующий пользователь → 401 Unauthorized
Тест 5: Доступ к защищённому профилю
- Метод: GET
- URL:
http://127.0.0.1:8000/api/auth/profile/ - Postman должен автоматически отправить куки из теста 3 (Cookies включены)
- Ожидаемый ответ: 200 OK, данные пользователя
- Без куки (очистить Cookies) → 403 Forbidden
Тест 6: Выход из аккаунта
- Метод: POST
- URL:
http://127.0.0.1:8000/api/auth/logout/ - Ожидаемый ответ: 200 OK, "Выход выполнен успешно."
- После этого GET /profile/ → 403 (куки удалены)
Тест 7: Проверка blacklist
- Вход → получить refresh-токен из куки
- Выход → refresh-токен добавлен в blacklist
- Попытка использовать старый refresh-токен через
POST /api/token/refresh/с body{"refresh": "..."}→ 401 Token is blacklisted
Связь с разделами курса
| Задание | Связанные разделы |
|---|---|
| Задание 1: Регистрация + валидация |
Теория → RegisterSerializer Примеры → serializers.py Ошибки → create() vs create_user() |
| Задание 2: Логин + JWT-токены |
Теория → LoginView Справочник → RefreshToken Ошибки → AllowAny на LoginView |
| Задание 3: Логаут + blacklist |
Теория → LogoutView Старый vs Новый → Blacklist Ошибки → token_blacklist в INSTALLED_APPS |