⚖️ Старый vs Новый: Auth-блок
⚡ Главные изменения
- Сигналы:
post_save.connect()→@receiverдекоратор - Токены вручную:
Token.objects.create()→ сигнал автоматически - JWT через сессии (устарело) → SimpleJWT + httpOnly-куки
- Разрешения через
if request.user.is_authenticated:в view →permission_classes datetime.utcnow()(deprecated Python 3.12) →datetime.now(timezone.utc)
1. Регистрация обработчиков сигналов
Старый способ (из лекции)
# Ручное подключение через .connect()
from django.db.models.signals import post_save
from .models import Book
def book_saved(sender, instance, created, **kwargs):
if created:
print(f'New book: {instance.title}')
# Подключение вне декоратора
post_save.connect(book_saved, sender=Book)
Современный способ (Django 5.x)
# Декоратор @receiver — чище и явнее
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Book
@receiver(post_save, sender=Book)
def book_saved(sender, instance, created, **kwargs):
if created:
print(f'New book: {instance.title}')
Оба варианта рабочие, но @receiver — идиоматичный Python-стиль и проще читать.
2. JWT: хранение токена в localStorage (устарело) vs httpOnly-куки
Старый подход (небезопасный)
// Клиентский JavaScript
// НЕБЕЗОПАСНО: localStorage уязвим для XSS
const response = await fetch('/api/token/', {...});
const data = await response.json();
localStorage.setItem('access_token', data.access);
// При каждом запросе вручную
fetch('/api/books/', {
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('access_token')
}
});
Современный подход (Django 5.x / SimpleJWT 5.x)
# Сервер сохраняет токены в httpOnly-куки
response.set_cookie(
key='access_token',
value=str(access_token),
httponly=True, # JS не имеет доступа → защита от XSS
secure=True, # только HTTPS в продакшн
samesite='Lax', # защита от CSRF
)
# JWTAuthenticationMiddleware автоматически
# добавляет токен из куки в заголовок Authorization
3. Авторизация: ручная проверка в view vs permission_classes
Старый способ (антипаттерн)
# Ручная проверка в каждом методе — DRY нарушен
class BookView(APIView):
def get(self, request):
if not request.user.is_authenticated:
return Response({'error': 'Not authenticated'}, status=401)
# бизнес-логика
...
def post(self, request):
if not request.user.is_authenticated:
return Response({'error': 'Not authenticated'}, status=401)
# бизнес-логика
...
Современный (DRF 3.15+)
# Декларативный подход через permission_classes
class BookView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request):
# DRF уже проверил аутентификацию
...
def post(self, request):
# DRF уже проверил аутентификацию
...
4. Создание токена при регистрации
Старый способ (из лекции)
# В view при создании пользователя вручную
user = User.objects.create_user(...)
token = Token.objects.create(user=user)
return Response({'token': token.key})
Современный (через сигнал)
# signals.py — автоматически при любом создании User
@receiver(post_save, sender=User)
def create_auth_token(sender, instance=None, created=False, **kwargs):
if created:
Token.objects.create(user=instance)
# View больше не знает о токене — разделение ответственности
5. datetime.utcnow() — устаревший API Python 3.12+
Из лекции (deprecated с Python 3.12)
from datetime import datetime
# Предупреждение: DeprecationWarning в Python 3.12+
access_expiry = datetime.utcfromtimestamp(access_token['exp'])
if datetime.utcnow() > expiry:
raise TokenError('expired')
Современный (Python 3.x aware datetime)
from datetime import datetime, timezone
# Timezone-aware datetime — правильный подход
access_expiry = datetime.fromtimestamp(access_token['exp'], tz=timezone.utc)
if datetime.now(tz=timezone.utc) > access_expiry:
raise TokenError('expired')
⚠️ Проверить по документации: поведение SimpleJWT при работе с временными метками может зависеть от версии. Проверьте актуальную документацию SimpleJWT 5.x.
6. Регистрация сигналов в apps.py
Старый способ (не надёжный)
# Импорт signals.py в models.py или __init__.py
# Вызывает проблемы с порядком импортов
# models.py — ПЛОХО
from . import signals # circular imports!
Современный (Django 5.x)
# apps.py — правильное место для импорта сигналов
class FirstAppConfig(AppConfig):
name = 'first_app'
def ready(self):
import first_app.signals # безопасно, вызывается после инициализации