💻 Примеры — Урок 44
⚡ Ключевые файлы проекта
Полный JWT-auth проект состоит из 4 файлов: settings.py, middleware.py, serializers.py, views.py, urls.py.
Основной паттерн: RefreshToken.for_user(user) → response.set_cookie() при логине/регистрации, и response.delete_cookie() при логауте.
Пример 1: Полный settings.py
# myproject/settings.py
from datetime import timedelta
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'rest_framework_simplejwt',
'rest_framework_simplejwt.token_blacklist', # обязательно для blacklist
'myapp',
]
# Middleware — JWTAuthenticationMiddleware ПОСЛЕ SecurityMiddleware
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'myapp.middleware.JWTAuthenticationMiddleware', # наш middleware
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
}
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',),
}
Пример 2: Полный middleware.py
# myapp/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):
"""
Middleware для автоматического использования JWT-токенов из куки.
Алгоритм:
1. Читаем access_token из куки
2. Если валиден — ставим HTTP_AUTHORIZATION
3. Если истёк — обновляем через refresh_token и ставим HTTP_AUTHORIZATION
4. В process_response — обновляем куку если был выпущен новый access
"""
def process_request(self, request):
access_token = request.COOKIES.get('access_token')
refresh_token = request.COOKIES.get('refresh_token')
if access_token:
try:
# Проверяем валидность access-токена
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 истёк — пробуем обновить
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 отсутствует — получаем новый
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):
"""Обновить access-токен через refresh-токен."""
if not refresh_token:
return None
try:
refresh = RefreshToken(refresh_token)
return str(refresh.access_token)
except TokenError:
return None
def process_response(self, request, response):
"""Обновить куку access_token если был выпущен новый."""
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)
Пример 3: serializers.py
# myapp/serializers.py
from rest_framework import serializers
from django.contrib.auth.models import User
from django.contrib.auth.password_validation import validate_password
from rest_framework.validators import UniqueValidator
class RegisterSerializer(serializers.ModelSerializer):
"""
Сериализатор для регистрации нового пользователя.
Поле password - только для записи, в ответе не возвращается.
"""
password = serializers.CharField(write_only=True)
class Meta:
model = User
fields = ['username', 'password', 'email']
def create(self, validated_data):
# create_user() хэширует пароль через set_password()
user = User.objects.create_user(
username=validated_data['username'],
password=validated_data['password'],
email=validated_data.get('email', '')
)
return user
⚠️ Проверить по документации: для расширенной валидации (уникальность email, сложность пароля) в продакшне следует добавить
UniqueValidator для поля email и validate_password из django.contrib.auth.password_validation. Базовый вариант из лекции этого не включает.
Пример 4: views.py — полный файл
# myapp/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, IsAuthenticated
from .serializers import RegisterSerializer
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, # True в продакшне
samesite='Lax',
expires=access_expiry
)
response.set_cookie(
key='refresh_token',
value=str(refresh_token),
httponly=True,
secure=False,
samesite='Lax',
expires=refresh_expiry
)
class LoginView(APIView):
"""Логин: аутентификация и выдача JWT-токенов в куках."""
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
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,
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
)
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
class RegisterView(APIView):
"""Регистрация нового пользователя с немедленной выдачей JWT-токенов."""
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
)
class ProtectedView(APIView):
"""Пример защищённого эндпоинта (IsAuthenticated по умолчанию)."""
permission_classes = [IsAuthenticated]
def get(self, request):
return Response({
'message': f'Hello, {request.user.username}! This is a protected resource.'
})
Пример 5: urls.py
# myapp/urls.py
from django.urls import path
from .views import LoginView, LogoutView, RegisterView, ProtectedView
urlpatterns = [
path('api/login/', LoginView.as_view(), name='login'),
path('api/logout/', LogoutView.as_view(), name='logout'),
path('api/register/', RegisterView.as_view(), name='register'),
path('protected/', ProtectedView.as_view(), name='protected'),
]
# myproject/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('myapp.urls')),
]
Пример 6: Тестирование через Postman
Шаг 1: Создание пользователя в Django shell
python manage.py shell
from django.contrib.auth.models import User
User.objects.create_superuser('admin', 'admin@example.com', 'password123')
Шаг 2: Логин
- Метод: POST
- URL:
http://127.0.0.1:8000/api/login/ - Body → form-data:
username=admin,password=password123 - Ожидаемый ответ: статус 200, в заголовках Set-Cookie —
access_tokenиrefresh_token
Шаг 3: Доступ к защищённому ресурсу
- Метод: GET
- URL:
http://127.0.0.1:8000/protected/ - Postman автоматически отправит куки (если Cookies включены в настройках)
- Ожидаемый ответ: 200, JSON с приветствием пользователя
Шаг 4: Регистрация
- Метод: POST
- URL:
http://127.0.0.1:8000/api/register/ - Body → form-data:
username=newuser,password=secret123,email=new@example.com - Ожидаемый ответ: 201 Created, JSON с username/email, Set-Cookie с токенами
Шаг 5: Логаут
- Метод: POST
- URL:
http://127.0.0.1:8000/api/logout/ - Ожидаемый ответ: 204 No Content, куки удалены