⚖️ Старый vs Новый — Урок 44
⚡ Главные изменения
- datetime.utcnow() (из лекции) →
datetime.now(timezone.utc)(Python 3.12+) - Токены в localStorage/body → httpOnly-куки (безопаснее от XSS)
- Нет blacklist при логауте → refresh.blacklist() + token_blacklist в INSTALLED_APPS
- SimpleJWT 4.x настройки → SimpleJWT 5.x переименованы в snake_case
Что изменилось с версиями Python и SimpleJWT
Лекция написана на SimpleJWT ранних версий. Основные паттерны сохраняются, но есть несколько важных изменений в Python 3.12+ и SimpleJWT 5.x, которые стоит знать.
1. datetime.utcnow() → timezone-aware datetime
Из лекции (устаревшее)
# Python < 3.12 — работает, но deprecated
from datetime import datetime
access_expiry = datetime.utcfromtimestamp(access_token['exp'])
refresh_expiry = datetime.utcfromtimestamp(refresh['exp'])
# В middleware
if datetime.utcfromtimestamp(token['exp']) < datetime.utcnow():
raise TokenError('Token expired')
Современное (Python 3.12+ / рекомендуется)
from datetime import datetime, timezone
# Конвертация exp в timezone-aware datetime
access_expiry = datetime.fromtimestamp(access_token['exp'], tz=timezone.utc)
refresh_expiry = datetime.fromtimestamp(refresh['exp'], tz=timezone.utc)
# Сравнение с текущим временем
if datetime.fromtimestamp(token['exp'], tz=timezone.utc) < datetime.now(timezone.utc):
raise TokenError('Token expired')
⚠️ В Python 3.12
datetime.utcnow() помечен как устаревший (DeprecationWarning). В Python 3.14 он будет удалён. Используйте datetime.now(timezone.utc).
На практике SimpleJWT сам проверяет срок действия токена при создании объекта
AccessToken(token_string) — если токен истёк, выбрасывается TokenError. Явная проверка exp в middleware из лекции избыточна, но помогает понять механизм.
2. Хранение токенов: localStorage → httpOnly cookies
Устаревший подход (небезопасно)
// Frontend: хранение в localStorage (НЕБЕЗОПАСНО!)
// XSS-атака может украсть токены через document.localStorage
localStorage.setItem('access_token', response.data.access);
localStorage.setItem('refresh_token', response.data.refresh);
// В запросах
fetch('/api/data/', {
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('access_token') }
});
Современный подход (httpOnly cookies)
# Backend: set_cookie с httponly=True (из лекции — правильный подход)
response.set_cookie(
key='access_token',
value=str(access_token),
httponly=True, # JavaScript не может читать эту куку
secure=True, # Только HTTPS в продакшне
samesite='Lax', # Защита от CSRF
expires=access_expiry
)
# Frontend: ничего дополнительного не нужно!
# Браузер автоматически отправляет httpOnly-куки с каждым запросом к домену
fetch('/api/data/', { credentials: 'include' });
Почему httpOnly лучше localStorage:
- localStorage уязвим к XSS: любой JS на странице может прочитать
localStorage.getItem() - httpOnly-куки недоступны из JS — XSS не помогает украсть токен
- Браузер автоматически управляет куками: отправляет, обновляет, удаляет по
expires
3. Логаут: удаление куки → добавление в blacklist
Из лекции (базовый вариант)
# Только удаляем куки — токен остаётся технически валидным до истечения
class LogoutView(APIView):
def post(self, request, *args, **kwargs):
response = Response(status=status.HTTP_204_NO_CONTENT)
response.delete_cookie('access_token')
response.delete_cookie('refresh_token')
return response
Современный вариант с blacklist
# settings.py
INSTALLED_APPS += ['rest_framework_simplejwt.token_blacklist']
# views.py — LogoutView с blacklist
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework_simplejwt.exceptions import TokenError
class LogoutView(APIView):
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-токен в blacklist
except TokenError:
pass # Токен уже истёк — нечего инвалидировать
response = Response(status=status.HTTP_204_NO_CONTENT)
response.delete_cookie('access_token')
response.delete_cookie('refresh_token')
return response
⚠️ Без blacklist: после логаута refresh-токен технически остаётся валидным до истечения срока (1 день). Злоумышленник, перехвативший refresh-токен, может получить новые access-токены. В ДЗ 20 (задание 3) требуется именно реализация с blacklist.
4. Настройки SIMPLE_JWT: SimpleJWT 4.x → 5.x
SimpleJWT 4.x (из лекции)
# Настройки использовали camelCase в некоторых местах
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',),
}
SimpleJWT 5.x (актуально)
# Все ключи — snake_case в UPPER_CASE; API обратно совместим
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',),
# Новые настройки в 5.x:
'TOKEN_OBTAIN_SERIALIZER': 'rest_framework_simplejwt.serializers.TokenObtainPairSerializer',
'TOKEN_REFRESH_SERIALIZER': 'rest_framework_simplejwt.serializers.TokenRefreshSerializer',
'TOKEN_VERIFY_SERIALIZER': 'rest_framework_simplejwt.serializers.TokenVerifySerializer',
}
⚠️ Проверить по документации: полный список настроек SimpleJWT 5.x доступен на django-rest-framework-simplejwt.readthedocs.io. Часть настроек появилась только в последних минорных версиях.
5. Middleware: MiddlewareMixin → чистый middleware-класс
Из лекции (MiddlewareMixin)
from django.utils.deprecation import MiddlewareMixin
class JWTAuthenticationMiddleware(MiddlewareMixin):
def process_request(self, request):
...
def process_response(self, request, response):
...
return response
Современный вариант (callable middleware)
class JWTAuthenticationMiddleware:
"""
Современный callable-стиль middleware (Django 1.10+).
Поддерживает async через async_capable = True.
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# Логика до обработки запроса (аналог process_request)
self._process_jwt_cookies(request)
response = self.get_response(request)
# Логика после обработки (аналог process_response)
self._update_access_cookie(request, response)
return response
def _process_jwt_cookies(self, request):
# ... та же логика, что в process_request
pass
def _update_access_cookie(self, request, response):
# ... та же логика, что в process_response
return response
MiddlewareMixin остаётся поддерживаемым и не устарел. Он полезен для обратной совместимости. Callable-стиль (через __call__) — это рекомендуемый подход для новых проектов, он лучше поддерживает ASGI и async Django.