📖 Теория — Урок 44

← К оглавлению урока

⚡ Ключевые концепции урока

  • Simple JWT — библиотека для работы с JWT в DRF; pip install djangorestframework-simplejwt
  • LoginView — authenticate() → RefreshToken.for_user() → set_cookie(access + refresh)
  • JWTAuthenticationMiddleware — извлекает токен из куки, подставляет в HTTP_AUTHORIZATION
  • Auto-refresh — если access истёк, middleware обновляет его через refresh-токен прозрачно
  • RegisterView — RegisterSerializer.save() → set_jwt_cookies() → 201 Created
  • LogoutView — delete_cookie('access_token') + delete_cookie('refresh_token')

Часть 1: Автосохранение и автоиспользование JWT токенов

В DRF можно реализовать систему, в которой JWT токены автоматически сохраняются при логине и затем используются для авторизации при запросах к другим эндпоинтам. Access и refresh токены хранятся в httpOnly-куках браузера, а специальное middleware прозрачно добавляет их в заголовок Authorization.

Установка Simple JWT

pip install djangorestframework-simplejwt

Настройка settings.py

# settings.py

INSTALLED_APPS = [
    # другие приложения
    'rest_framework_simplejwt',
    'rest_framework_simplejwt.token_blacklist',  # для BLACKLIST_AFTER_ROTATION
]

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
}

from datetime import timedelta

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',),
}
Важно: если BLACKLIST_AFTER_ROTATION = True, необходимо добавить rest_framework_simplejwt.token_blacklist в INSTALLED_APPS и выполнить python manage.py migrate. Без этого приложение упадёт с ошибкой при первом же refresh-запросе.
ПараметрЗначение по умолчаниюОписание
ACCESS_TOKEN_LIFETIME5 минутСрок жизни access-токена
REFRESH_TOKEN_LIFETIME1 деньСрок жизни refresh-токена
ROTATE_REFRESH_TOKENSFalseПри обновлении выдавать новый refresh-токен
BLACKLIST_AFTER_ROTATIONFalseАннулировать старый refresh после ротации
AUTH_HEADER_TYPES('Bearer',)Тип схемы в заголовке Authorization

Часть 2: LoginView — логин с сохранением токенов в куках

Представление получает username/password, аутентифицирует пользователя, генерирует токены и сохраняет их в httpOnly-куках ответа.

# 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.permissions import AllowAny

class LoginView(APIView):
    permission_classes = [AllowAny]

    def post(self, request, *args, **kwargs):
        username = request.data.get('username')
        password = request.data.get('password')
        user = authenticate(request, username=username, password=password)

        if user:
            refresh = RefreshToken.for_user(user)
            access_token = refresh.access_token

            # Используем exp для установки времени истечения куки
            access_expiry = datetime.fromtimestamp(access_token['exp'], tz=timezone.utc)
            refresh_expiry = datetime.fromtimestamp(refresh['exp'], tz=timezone.utc)

            response = Response(status=status.HTTP_200_OK)
            response.set_cookie(
                key='access_token',
                value=str(access_token),
                httponly=True,
                secure=False,   # Используйте True для HTTPS в продакшне
                samesite='Lax',
                expires=access_expiry
            )
            response.set_cookie(
                key='refresh_token',
                value=str(refresh),
                httponly=True,
                secure=False,
                samesite='Lax',
                expires=refresh_expiry
            )
            return response
        else:
            return Response(
                {"detail": "Invalid credentials"},
                status=status.HTTP_401_UNAUTHORIZED
            )
# urls.py

from django.urls import path
from .views import LoginView

urlpatterns = [
    path('api/login/', LoginView.as_view(), name='login'),
]
Почему httpOnly? Флаг httponly=True запрещает JavaScript читать куку через document.cookie. Это защищает токены от XSS-атак. Браузер автоматически отправляет httpOnly-куки с каждым запросом к домену.
Параметр set_cookieЗначениеЗачем
httponly=TrueобязательноЗащита от XSS
secure=Trueв продакшнеТолько через HTTPS
samesite='Lax'рекомендуетсяЗащита от CSRF; куки отправляются при навигации, но не при AJAX с другого домена
expiresdatetime из token['exp']Автоматическое удаление куки после истечения токена

Часть 3: JWTAuthenticationMiddleware — автоматическое использование токенов

Middleware извлекает токены из куки при каждом входящем запросе и добавляет их в заголовок HTTP_AUTHORIZATION. Это позволяет стандартному механизму JWTAuthentication DRF проверять токен прозрачно.

# middleware.py

from datetime import datetime, timezone
from django.utils.deprecation import MiddlewareMixin
from rest_framework_simplejwt.tokens import RefreshToken, AccessToken
from rest_framework_simplejwt.exceptions import TokenError

class JWTAuthenticationMiddleware(MiddlewareMixin):

    def process_request(self, request):
        access_token = request.COOKIES.get('access_token')
        refresh_token = request.COOKIES.get('refresh_token')

        if access_token:
            try:
                token = AccessToken(access_token)
                if datetime.fromtimestamp(token['exp'], tz=timezone.utc) < datetime.now(timezone.utc):
                    raise TokenError('Token expired')
                request.META['HTTP_AUTHORIZATION'] = f'Bearer {access_token}'
            except TokenError:
                # access истёк — пробуем обновить через refresh
                new_access_token = self.refresh_access_token(refresh_token)
                if new_access_token:
                    request.META['HTTP_AUTHORIZATION'] = f'Bearer {new_access_token}'
                    request._new_access_token = new_access_token
                else:
                    # refresh тоже недействителен — очищаем куки
                    self.clear_cookies(request)
        elif refresh_token:
            # access нет, но refresh есть — получаем новый access
            new_access_token = self.refresh_access_token(refresh_token)
            if new_access_token:
                request.META['HTTP_AUTHORIZATION'] = f'Bearer {new_access_token}'
                request._new_access_token = new_access_token
            else:
                self.clear_cookies(request)

    def refresh_access_token(self, refresh_token):
        try:
            refresh = RefreshToken(refresh_token)
            new_access_token = str(refresh.access_token)
            return new_access_token
        except TokenError:
            return None

    def process_response(self, request, response):
        new_access_token = getattr(request, '_new_access_token', None)
        if new_access_token:
            access_expiry = AccessToken(new_access_token)['exp']
            response.set_cookie(
                key='access_token',
                value=new_access_token,
                httponly=True,
                secure=False,   # True в продакшне
                samesite='Lax',
                expires=datetime.fromtimestamp(access_expiry, tz=timezone.utc)
            )
        return response

    def clear_cookies(self, request):
        """Метод для удаления истекших куки из объекта запроса."""
        request.COOKIES.pop('access_token', None)
        request.COOKIES.pop('refresh_token', None)
# settings.py — добавить middleware

MIDDLEWARE = [
    # другие промежуточные ПО
    'your_app.middleware.JWTAuthenticationMiddleware',
]
Порядок работы middleware:
  1. process_request: читает куки → проверяет access → обновляет через refresh если нужно → ставит HTTP_AUTHORIZATION
  2. DRF: JWTAuthentication читает HTTP_AUTHORIZATION → аутентифицирует пользователя
  3. process_response: если был выпущен новый access — обновляет куку в ответе

Логика принятия решений middleware

Состояние кукиДействие
access валиденHTTP_AUTHORIZATION = Bearer {access}
access истёк + refresh валиденНовый access → HTTP_AUTHORIZATION; обновить куку в ответе
access истёк + refresh истёкОчистить куки; запрос идёт без Authorization
access нет + refresh естьПолучить новый access; поставить Authorization и куку
Оба отсутствуютНичего не делать; запрос анонимный

Часть 4: LogoutView — удаление токенов из куки

# views.py

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status

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
# urls.py

from django.urls import path
from .views import LogoutView

urlpatterns = [
    path('api/logout/', LogoutView.as_view(), name='logout'),
]
⚠️ В данной реализации из лекции токены только удаляются из куки, но не помещаются в blacklist. Если access-токен ещё действителен (например, 4 минуты из 5), он останется рабочим до истечения срока. Для полной безопасности логаута следует добавить инвалидацию refresh-токена через blacklist. Это расширение рассматривается в ДЗ 20 (задание 3) и в разделе Ошибки.

Часть 5: Пример использования через Postman

1. Логин

  1. Создайте новый POST-запрос в Postman
  2. URL: http://127.0.0.1:8000/api/login/
  3. Body → form-data: поля username и password
  4. Send → в ответе: куки access_token и refresh_token

2. Доступ к защищённому ресурсу

  1. GET-запрос на http://127.0.0.1:8000/protected/
  2. Postman автоматически подставит куки из предыдущего запроса
  3. Middleware извлечёт access-токен и поставит его в Authorization
  4. DRF аутентифицирует пользователя → ответ 200 OK

3. Логаут

  1. POST на http://127.0.0.1:8000/api/logout/
  2. Токены удалены из куки → последующие запросы анонимны

Часть 6: Регистрация пользователя с JWT

При регистрации создаётся новый пользователь, и сразу же выдаются JWT-токены — пользователь немедленно становится аутентифицированным без необходимости отдельного логина.

RegisterSerializer

Сериализатор обрабатывает входные данные, проверяет их и создаёт нового пользователя через create_user() (который хэширует пароль).

# serializers.py

from rest_framework import serializers
from django.contrib.auth.models import User

class RegisterSerializer(serializers.ModelSerializer):
    password = serializers.CharField(write_only=True)

    class Meta:
        model = User
        fields = ['username', 'password', 'email']

    def create(self, validated_data):
        user = User.objects.create_user(
            username=validated_data['username'],
            password=validated_data['password'],
            email=validated_data.get('email', '')
        )
        return user
Почему create_user(), а не create()? Метод create_user() автоматически хэширует пароль через set_password(). Если использовать create() напрямую, пароль сохранится в открытом виде и аутентификация перестанет работать.

Вспомогательная функция set_jwt_cookies()

Выносим логику создания куки в отдельную функцию, чтобы не дублировать её в LoginView и RegisterView:

# views.py

from datetime import datetime, timezone
from rest_framework_simplejwt.tokens import RefreshToken

def set_jwt_cookies(response, user):
    """Устанавливает JWT токены в куки ответа."""
    refresh_token = RefreshToken.for_user(user)
    access_token = refresh_token.access_token

    access_expiry = datetime.fromtimestamp(access_token['exp'], tz=timezone.utc)
    refresh_expiry = datetime.fromtimestamp(refresh_token['exp'], tz=timezone.utc)

    response.set_cookie(
        key='access_token',
        value=str(access_token),
        httponly=True,
        secure=False,
        samesite='Lax',
        expires=access_expiry
    )
    response.set_cookie(
        key='refresh_token',
        value=str(refresh_token),
        httponly=True,
        secure=False,
        samesite='Lax',
        expires=refresh_expiry
    )

RegisterView

# views.py

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .serializers import RegisterSerializer

class RegisterView(APIView):
    permission_classes = [AllowAny]

    def post(self, request):
        serializer = RegisterSerializer(data=request.data)
        if serializer.is_valid():
            user = serializer.save()
            response = Response({
                'user': {
                    'username': user.username,
                    'email': user.email
                }
            }, status=status.HTTP_201_CREATED)
            set_jwt_cookies(response, user)
            return response
        else:
            return Response(
                serializer.errors,
                status=status.HTTP_400_BAD_REQUEST
            )
# urls.py

from django.urls import path
from .views import RegisterView

urlpatterns = [
    path('api/register/', RegisterView.as_view(), name='register'),
]

Пример использования через Postman

  1. POST на http://127.0.0.1:8000/api/register/
  2. Body → form-data: поля username, password, email
  3. Send → статус 201 Created, JSON с данными пользователя, куки с токенами
Итог: Этот подход позволяет создать систему регистрации пользователей через API в DRF с использованием JWT, предоставляя возможность регистрации и аутентификации в одном шаге. Пользователь получает токены сразу после создания аккаунта.