📖 Теория — Урок 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_LIFETIME | 5 минут | Срок жизни access-токена |
REFRESH_TOKEN_LIFETIME | 1 день | Срок жизни refresh-токена |
ROTATE_REFRESH_TOKENS | False | При обновлении выдавать новый refresh-токен |
BLACKLIST_AFTER_ROTATION | False | Аннулировать старый 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 с другого домена |
expires | datetime из 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:
process_request: читает куки → проверяет access → обновляет через refresh если нужно → ставит HTTP_AUTHORIZATION- DRF: JWTAuthentication читает HTTP_AUTHORIZATION → аутентифицирует пользователя
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. Логин
- Создайте новый POST-запрос в Postman
- URL:
http://127.0.0.1:8000/api/login/ - Body → form-data: поля
usernameиpassword - Send → в ответе: куки
access_tokenиrefresh_token
2. Доступ к защищённому ресурсу
- GET-запрос на
http://127.0.0.1:8000/protected/ - Postman автоматически подставит куки из предыдущего запроса
- Middleware извлечёт access-токен и поставит его в Authorization
- DRF аутентифицирует пользователя → ответ 200 OK
3. Логаут
- POST на
http://127.0.0.1:8000/api/logout/ - Токены удалены из куки → последующие запросы анонимны
Часть 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
- POST на
http://127.0.0.1:8000/api/register/ - Body → form-data: поля
username,password,email - Send → статус 201 Created, JSON с данными пользователя, куки с токенами
Итог: Этот подход позволяет создать систему регистрации пользователей через API в DRF с использованием JWT, предоставляя возможность регистрации и аутентификации в одном шаге. Пользователь получает токены сразу после создания аккаунта.