⚖️ Старый 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.