Мета
Уявіть, що ви створюєте безпечний шлюз для додатка, використовуючи Django та Django REST Framework. Ваше завдання — створити REST API, який оброблятиме аутентифікацію та авторизацію користувачів. Ось що він буде робити:
Він тепло привітає нових користувачів, дозволяючи їм реєструватися. Після того, як вони стануть частиною системи, вони зможуть легко увійти. Для додаткової безпеки користувачі зможуть оновлювати свої токени для підтримки аутентифікації.
Якщо користувач вирішить зробити перерву, він може вийти в будь-який час. Система також дозволить користувачам переглядати та оновлювати свої особисті дані, коли їм це потрібно, забезпечуючи безперешкодний і зручний досвід.
Повний код можна завантажити з мого репозиторію на GitHub.
Передумови
- Ubuntu (або WSL для користувачів Windows)
- Python 3.x
- Django
- Django REST Framework
- PyJWT
- Constance — динамічні налаштування Django
- Redis
Щоб встановити залежності та інструменти для цього проекту, ми використаємо менеджер пакетів uv. Спочатку ініціалізуємо новий проект, а потім додаємо всі необхідні залежності.
uv init
uv add django djangorestframework pyjwt django-constance redis
Після ініціалізації проекту та додавання залежностей, наш файл pyproject.toml
повинен виглядати ось так:
[project]
name = "school-django-auth"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"django-constance>=4.1.3",
"django>=5.1.4",
"djangorestframework>=3.15.2",
"pyjwt>=2.10.1",
"redis>=5.2.1",
]
За допомогою менеджера пакетів uv
ми можемо розпочати новий проект Django та створити додаток. Використовуйте наступні команди:
uv run django-admin startproject auth_project
cd auth_project
uv run manage.py startapp auth_api
Після виконання цих команд структура проекту повинна виглядати ось так:
├── auth_project
│ ├── auth_api
│ │ ├── admin.py
│ │ ├── apps.py
│ │ ├── __init__.py
│ │ ├── `migrations`
│ │ │ └── __init__.py
│ │ ├── models.py
│ │ ├── tests.py
│ │ └── views.py
│ ├── auth_project
│ │ ├── asgi.py
│ │ ├── __init__.py
│ │ ├── settings.py
│ │ ├── urls.py
│ │ └── wsgi.py
│ └── manage.py
├── hello.py
├── pyproject.toml
├── README.md
└── uv.lock
Оновіть settings.py
, додавши rest_framework
та auth_api
до списку INSTALLED_APPS
:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'auth_app',
]
Щоб увімкнути аутентифікацію та дозволи в нашому Django-додатку, потрібно налаштувати параметри Django REST Framework. Ці налаштування вказують метод аутентифікації та стандартні дозволи для доступу до API.
Додайте наступний код до settings.py
:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'auth_api.authentication.JWTAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
}
Як ви, мабуть, помітили, до проекту були додані залежності django-constance та redis. Django-Constance — це потужний інструмент, який дозволяє керувати динамічними налаштуваннями конфігурації безпосередньо в додатку Django без необхідності міграцій бази даних. У поєднанні з Redis, високопродуктивним сховищем даних в пам'яті, це забезпечує швидке та ефективне зберігання та отримання цих конфігурацій.
Redis взаємодіє з Django-Constance, служачи бекендом, де зберігаються значення конфігурацій.
Це налаштування робить оновлення конфігурацій миттєвими та високо масштабованими, що є ідеальним для динамічних, високонавантажених додатків.
Щоб налаштувати тривалість токенів динамічно, додайте наступне в ваш settings.py
:
CONSTANCE_CONFIG = {
'ACCESS_TOKEN_LIFETIME': (30 * 60 * 60, 'Час життя токена доступу в секундах'),
'REFRESH_TOKEN_LIFETIME': (30 * 24 * 60 * 60, 'Час життя токена оновлення в секундах'),
}
Що це робить:
Токени доступу та токени оновлення є важливими компонентами сучасних систем аутентифікації, особливо при роботі з безстатевими API. Ось короткий огляд:
Токени доступу використовуються для аутентифікації API-запитів від імені користувача, зазвичай вони мають короткий термін дії з метою безпеки (наприклад, ACCESS_TOKEN_LIFETIME
в цьому прикладі встановлено на 30 годин). Вони передаються з кожним API-запитом, зазвичай у заголовку Authorization для підтвердження особи та прав користувача. Якщо токен буде скомпрометовано, він буде дійсним лише короткий час, що мінімізує можливу шкоду.
Токени оновлення використовуються для отримання нових токенів доступу без необхідності повторної аутентифікації користувача. Вони мають довший термін дії, ніж токени доступу (наприклад, REFRESH_TOKEN_LIFETIME
тут складає 30 днів).
Щоб реалізувати токени оновлення в нашому Django-додатку, ми додамо модель для їх зберігання. Відкрийте auth_api/models.py
і додайте наступний код:
import uuid
from django.conf import settings
class RefreshToken(models.Model):
token = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
user = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='refresh_tokens')
created_at = models.DateTimeField(auto_now_add=True)
expires_at = models.DateTimeField()
def is_valid(self):
from django.utils.timezone import now
return now() < self.expires_at
Щоб побудувати Django-додаток, який вимагає більшої гнучкості в управлінні користувачами, ніж це дозволяє стандартна модель User
, ми використаємо кастомну модель користувача
. Цей підхід дозволяє точно визначити, як користувачі повинні бути представлені та аутентифіковані в вашому додатку.
У файлі auth_api/models.py
додайте наступний код:
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
from django.utils.translation import gettext_lazy as _
from django.db import models
# Кастомний менеджер для створення користувачів
class CustomUserManager(BaseUserManager):
def create_user(self, email, password=None, **extra_fields):
if not email:
raise ValueError(_('Поле Email повинно бути заповнене'))
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email, password=None, username='', **extra_fields):
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
if not extra_fields.get('is_staff'):
raise ValueError(_('Суперкористувач повинен мати is_staff=True'))
if not extra_fields.get('is_superuser'):
raise ValueError(_('Суперкористувач повинен мати is_superuser=True'))
return self.create_user(email, password, **extra_fields)
class CustomUser(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(unique=True)
username = models.CharField(max_length=150, blank=True, default='')
first_name = models.CharField(max_length=150, blank=True)
last_name = models.CharField(max_length=150, blank=True)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
objects = CustomUserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
def __str__(self):
return self.email
Що тут відбувається?
CustomUserManager: Уявіть собі це як "менеджер" для створення користувачів. Він визначає правила для додавання як звичайних користувачів, так і суперкористувачів, забезпечуючи правильне налаштування всіх параметрів. CustomUser: Це серце вашої моделі користувача.
Замість того, щоб використовувати імена користувачів, система використовує електронну пошту як унікальний ідентифікатор (USERNAME_FIELD). Це більш сучасний і гнучкий спосіб керування обліковими записами користувачів.
З такою настройкою ви можете легко додавати або змінювати поля, такі як firstname, lastname або будь-які інші деталі, специфічні для потреб вашого додатку.
Повний код файлу auth_api/models.py
виглядатиме так:
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
from django.utils.translation import gettext_lazy as _
import uuid
from django.conf import settings
from django.db import models
# Користувацьке створення користувачів
class CustomUserManager(BaseUserManager):
def create_user(self, email, password=None, **extra_fields):
if not email:
raise ValueError(_('Поле Email має бути заповнене'))
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email, password=None, username='', **extra_fields):
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
if not extra_fields.get('is_staff'):
raise ValueError(_('Superuser має бути з is_staff=True'))
if not extra_fields.get('is_superuser'):
raise ValueError(_('Superuser має бути з is_superuser=True'))
return self.create_user(email, password, **extra_fields)
class CustomUser(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(unique=True)
username = models.CharField(max_length=150, blank=True, default='')
first_name = models.CharField(max_length=150, blank=True)
last_name = models.CharField(max_length=150, blank=True)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
objects = CustomUserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
def __str__(self):
return self.email
# Клас Refresh token
class RefreshToken(models.Model):
token = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
user = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='refresh_tokens')
created_at = models.DateTimeField(auto_now_add=True)
expires_at = models.DateTimeField()
def is_valid(self):
from django.utils.timezone import now
return now() < self.expires_at
Щоб використовувати кастомну модель користувача у вашому Django додатку, потрібно вказати, яку модель використовувати. Це робиться шляхом налаштування конфігурації AUTH_USER_MODEL
у файлі settings.py
.
Додайте наступний рядок до вашого settings.py
:
AUTH_USER_MODEL = 'auth_api.CustomUser'
Для обробки аутентифікації на основі JWT у нашому додатку ми створимо кастомний клас аутентифікації.
Цей клас перевірятиме токени, які надаються в API запитах, і аутентифікуватиме користувачів на їх основі.
Створіть новий файл auth_api/authentication.py
і вставте наступний код:
import jwt
from django.conf import settings
from .models import CustomUser
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
class JWTAuthentication(BaseAuthentication):
def authenticate(self, request):
auth_header = request.headers.get('Authorization')
if not auth_header:
return None
try:
prefix, token = auth_header.split()
if prefix != 'Bearer':
raise AuthenticationFailed('Невірний префікс токену')
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=['HS256'])
user = CustomUser.objects.get(id=payload['user_id'])
return (user, None)
except jwt.ExpiredSignatureError:
raise AuthenticationFailed('Токен прострочений!!!!!!')
except jwt.DecodeError:
raise AuthenticationFailed('Невірний токен')
except (jwt.InvalidTokenError, CustomUser.DoesNotExist):
raise AuthenticationFailed('Невірні облікові дані')
Далі ми створимо серіалізатори для обробки даних користувачів та процесів аутентифікації в додатку Django REST Framework (DRF). Серіалізатори спрощують процес перетворення складних даних, таких як моделі Django, у формат JSON для API та валідацію вхідних даних.
Ось що робить кожен серіалізатор:
UserSerializer
: Використовується для отримання та відображення основних даних користувача, таких як id, username та email.RegisterSerializer
: Керує реєстрацією користувачів, приймаючи email та пароль, забезпечуючи їх безпечне збереження та доступ лише на запис.LoginSerializer
: Валідує облікові дані користувача, перевіряючи надані email та пароль.
Ці серіалізатори є основою системи керування користувачами вашого API, спрощуючи взаємодію з моделлю CustomUser
.
Створіть файл auth_api/serializers.py
і вставте:
from .models import CustomUser
from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = CustomUser
fields = ['id', 'username', 'email']
class RegisterSerializer(serializers.ModelSerializer):
class Meta:
model = CustomUser
fields = ['email', 'password']
extra_kwargs = {'password': {'write_only': True}}
def create(self, validated_data):
user = CustomUser.objects.create_user(
validated_data['email'], validated_data['password'])
return user
class LoginSerializer(serializers.Serializer):
email = serializers.EmailField()
password = serializers.CharField(write_only=True)
Тепер перейдемо до реалізації API-поглядів для аутентифікації користувачів та керування профілями. Ці погляди будуть обробляти ключові операції, такі як реєстрація користувачів, вхід, оновлення токену, вихід та оновлення профілю, формуючи основу нашої системи аутентифікації.
Ось розбір основних компонентів:
create_access_token
: Допоміжна функція, яка генерує JWT токен доступу з конкретним часом закінчення терміну дії, який динамічно отримується з налаштувань django-constance.RegisterView
: Обробляє реєстрацію користувачів. Вона валідує вхідні дані, зберігає нового користувача та повертає його дані після успішної реєстрації.LoginView
: Аутентифікує користувачів за допомогою їх email та пароля. У разі успіху генерує токен доступу та токен оновлення, що дозволяє безперешкодно керувати сесіями.RefreshView
: Дозволяє користувачам оновити свій токен доступу, використовуючи дійсний токен оновлення.
Цей клас генерує нові токени та робить недійсними старі токени оновлення.LogoutView
: Виходить з користувачів, видаляючи їх токен оновлення, тим самим забезпечуючи безпечне завершення сесії.ProfileView
: Дозволяє автентифікованим користувачам переглядати (GET) або оновлювати (PUT) свою профільну інформацію.
import jwt
import datetime
from django.conf import settings
from django.utils.timezone import now, timedelta
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated, AllowAny
from django.contrib.auth import authenticate
from constance import config
from .models import RefreshToken
from .serializers import UserSerializer, RegisterSerializer, LoginSerializer
def create_access_token(user):
print(f"ACCESS_TOKEN_LIFETIME: {config.ACCESS_TOKEN_LIFETIME}")
return jwt.encode({
'user_id': user.id,
'exp': datetime.datetime.now(datetime.timezone.utc) + timedelta(seconds=config.ACCESS_TOKEN_LIFETIME)
}, settings.SECRET_KEY, algorithm='HS256')
class RegisterView(APIView):
permission_classes = [AllowAny]
def post(self, request):
serializer = RegisterSerializer(data=request.data)
if serializer.is_valid():
user = serializer.save()
return Response(UserSerializer(user).data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class LoginView(APIView):
permission_classes = [AllowAny]
def post(self, request):
serializer = LoginSerializer(data=request.data)
if serializer.is_valid():
user = authenticate(
email=serializer.validated_data['email'], password=serializer.validated_data['password'])
if user:
access_token = create_access_token(user)
refresh_token = RefreshToken.objects.create(
user=user,
expires_at=now() + timedelta(seconds=config.REFRESH_TOKEN_LIFETIME)
)
return Response({
'access_token': access_token,
'refresh_token': refresh_token.token
}, status=status.HTTP_200_OK)
return Response({'error': 'Невірні облікові дані'}, status=status.HTTP_401_UNAUTHORIZED)
class RefreshView(APIView):
permission_classes = [AllowAny]
def post(self, request):
refresh_token = request.data.get('refresh_token')
try:
token_obj = RefreshToken.objects.get(token=refresh_token)
if token_obj.is_valid():
new_access_token = create_access_token(token_obj.user)
new_refresh_token = RefreshToken.objects.create(
user=token_obj.user,
expires_at=now() + timedelta(seconds=config.REFRESH_TOKEN_LIFETIME)
)
token_obj.delete()
return Response({
'access_token': new_access_token,
'refresh_token': str(new_refresh_token.token)
})
except RefreshToken.DoesNotExist:
return Response({'error': 'Невірний refresh token'}, status=status.HTTP_400_BAD_REQUEST)
class LogoutView(APIView):
permission_classes = [AllowAny]
def post(self, request):
refresh_token = request.data.get('refresh_token')
try:
token_obj = RefreshToken.objects.get(token=refresh_token)
token_obj.delete()
return Response({'success': 'Користувач вийшов.'}, status=status.HTTP_200_OK)
except RefreshToken.DoesNotExist:
return Response({'error': 'Невірний refresh token'}, status=status.HTTP_400_BAD_REQUEST)
class ProfileView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request):
return Response(UserSerializer(request.user).data)
def put(self, request):
serializer = UserSerializer(
request.user, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Створіть файл auth_api/urls.py
та визначте маршрути для додатку:
from django.urls import path
from .views import RegisterView, LoginView, RefreshView, LogoutView, ProfileView
urlpatterns = [
Щоб створити `міграції`, виконайте наступну команду:
uv run manage.py makemigrations
```
Ця команда перевіряє ваші моделі і генерує необхідні файли міграцій для застосування змін до бази даних.
Застосування міграцій:
Після того як файли міграцій створено, їх потрібно застосувати до бази даних. Цей крок забезпечує відповідність схеми бази даних визначенням моделей. Щоб застосувати міграції
, виконайте:
uv run manage.py migrate
Для цього простого випадку ми використовуємо базу даних sqlite, яка є легкою і не потребує додаткової конфігурації.
Застосування нашого коду
Спочатку давайте запустимо сервер. За допомогою менеджера uv
відкрийте термінал і введіть:
uv run manage.py runserver
Виведення буде наступним:
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
January 22, 2025 - 10:45:42
Django version 5.1.4, using settings 'auth_project.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
Пам'ятайте, що на початку ми говорили про Redis, необхідну залежність для роботи Django Constance? Залежно від вашої системи (Ubuntu або WSL) процес установки виглядатиме так:
sudo apt update
sudo apt install redis
Перевірте на Ubuntu, чи працює сервер Redis:
sudo systemctl status redis
Він повинен бути в статусі active
. Якщо він не активний, запустіть його за допомогою:
sudo systemctl start redis
Якщо ви користуєтесь WSL, запустіть службу в іншому вікні термінала, виконавши:
redis-server
Відкрийте ще одне вікно термінала і перевірте наші кінцеві точки:
Реєстрація користувача
curl -X POST http://localhost:8000/api/register -d '{"email": "[email protected]", "password": "securepassword"}' -H "Content-Type: application/json"
Виведення:
{"id":1,"email":"[email protected]"}
Аутентифікація. Тут ми отримаємо Access та Refresh токени:
curl -X POST http://localhost:8000/api/login/ -d '{"email": "[email protected]", "password": "securepassword"}' -H "Content-Type: application/json"
Виведення:
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE3Mzc2NzE1NTF9.qZ8vlF65ovei8hk0mqqshjlIsEzFApZZWTIAdAUib4g","refresh_token":"c9c1a1fd-f886-404a-862b-2c123b0ec1a5"}
Оновлення Access Token
curl -X POST http://localhost:8000/api/refresh/ -d '{"refresh_token": "c9c1a1fd-f886-404a-862b-2c123b0ec1a5"}' -H "Content-Type: application/json"
Виведення:
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE3Mzc2NzE3MjR9.2PtaZVq5ioGNyQoPDU6U2ED-sgALWJZAq1Kw9-UL8no","refresh_token":"2f37b70f-18dd-471c-9ef7-8065cb2f4634"}
Вихід з системи (Інвалідація Refresh токену)
curl -X POST http://localhost:8000/api/logout/ -d '{"refresh_token": "2f37b70f-18dd-471c-9ef7-8065cb2f4634"}' -H "Content-Type: application/json"
Виведення:
{"success":"User logged out."}
Отримання особистої інформації
curl -X GET http://localhost:8000/api/me/ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE3Mzc2NzE3MjR9.2PtaZVq5ioGNyQoPDU6U2ED-sgALWJZAq1Kw9-UL8no"
Виведення:
{"id":1,"username":"","email":"[email protected]"}
Оновлення особистої інформації
curl -X PUT http://localhost:8000/api/me/ -d '{"email": "[email protected]", "username": "John Doe"}' -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE3Mzc2NzE3MjR9.2PtaZVq5ioGNyQoPDU6U2ED-sgALWJZAq1Kw9-UL8no" -H "Content-Type: application/json"
Виведення:
{"id":1,"username":"John Doe","email":"[email protected]"}
Аутентифікація та авторизація в Django і Django REST Framework
Перекладено з: [Authentication, Authorization with Django and Django REST Framework](https://medium.com/@rodionatamaniuc/authentication-authorization-with-django-and-django-rest-framework-a1fd0f711fec)