[Python, Django] Настройка аутентификации JWT в новом проекте Django
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Данная статья является сборкой-компиляцией нескольких (основано на первой) статей, как результат моих изучений по теме jwt аутентификации в джанге со всем вытекающим. Так и не удалось (по крайней мере в рунете) найти нормальную статью, в которой рассказывается от этапа создания проекта, startproject, прикручивание jwt аутентификации.Добротно исследовав, отдаю на людской суд.Ссылки на пользуемые статьи прилагаются:
- https://thinkster.io/tutorials/django-json-api/authentication
- https://simpleisbetterthancomplex.com/tutorial/2018/12/19/how-to-use-jwt-authentication-with-django-rest-framework.html
- https://www.django-rest-framework.org/api-guide/authentication/
- https://medium.com/django-rest/django-rest-framework-jwt-authentication-94bee36f2af8
Настройка аутентификации JWTDjango поставляется с системой аутентификации, основанной на сеансах, и это работает из коробки. Это включает в себя все модели (models), представления (views) и шаблоны (templates), которые могут быть нужны вам для создания и дальнейшего логина пользователей. Но вот в чем загвоздка: стандартная система аутентификации Django работает только с традиционным 'запрос-ответ' циклом HTML.Что мы имеем ввиду под «традиционным 'запрос-ответ' циклом HTML»? Исторически, когда пользователь хотел выполнить какое-то действие (например, создать новый аккаунт), он заполнял определенную форму в браузере. Далее, когда он кликал на кнопку «Отправить», браузер формировал запрос - который включал в себя данные, введенные пользователем - и отправлял на сервер, сервер обрабатывал запрос, и отвечал либо HTML страницей, либо редиректом на новую страницу. Это то, что мы имеем ввиду, когда говорим о «полном обновлении страницы».Почему важно знать, что встроенная система аутентификации Django работает только с традиционным 'запрос-ответ' циклом HTML? Потому что клиент, для которого мы создадим данный API, не придерживается этого цикла. Вместо этого, клиент будет ожидать, что сервер вернет JSON, вместе обычного HTML. Возвращая JSON, мы можем позволить решать клиенту, а не серверу, что делать дальше. В цикле 'запрос-ответ' JSON, сервер получает данные, обрабатывает их и возвращает ответ (пока что как и в цикле 'запрос-ответ' HTML), но ответ не управляет поведением браузера. Ответ просто сообщает браузеру результат запроса.К счастью, команда разработки Django поняла, что тренды веб разработки движутся именно в этом направлении. Они также знали, что некоторые проекты могут не захотеть использовать встроенные модели, представления и шаблоны. Вместо этого, они могут использовать собственные. Чтобы убедиться, что все усилия, затраченные на создание встроенной системы аутентификации Django, не потрачены зря, они решили сделать возможным использование наиболее важных частей, сохраняя при этом возможность настройки конечного результата.Мы поговорим об этом позже в этом руководстве, а пока что вот список того, что вам нужно знать:
- Мы создадим собственную модель User, взамен модели Django
- Нам нужно будет написать наши собственные представления для поддержки возврата JSON вместо HTML
- Поскольку мы не будем использовать HTML, нам не нужны встроенные шаблоны входа и регистрации Django
Аутентификация, основанная на сессииПо умолчанию, Django использует сессии для аутентификации. Прежде чем идти дальше, нужно проговорить, что это значит, почему это важно, что такое аутентификация на основе токенов и что такое JSON Web Token Authentication (JWT для краткости), и что из всего этого мы будем использовать далее в статье.В Django сессии хранятся в файлах куки (cookie). Эти сессии, наряду со встроенным промежуточным ПО (middlewares) и объектами запросов, гарантируют, что пользователь будет доступен в каждом запросе. Доступ к пользователю можно получить как request.user. Когда пользователь вошел в систему, request.user является экземпляром класса User. Когда же он разлогинивается, request.user является экземпляром класса AnonymousUser. Независимо от того, аутентифицирован пользователь или нет, request.user всегда будет существовать.В чем же разница? Говоря просто, в любое время, когда вы хотите узнать, является ли текущий пользователь аутентифицированным, вы можете использовать request.user.isauthenticated(), который вернет True в случае аутентификации пользователя и False в обратно случае. Если request.user является AnonymousUser, request.user.isauthenticated() вернет False. Это позволяет разработчику (вам :) ) преобразоватьif request.user is not None and request.user.isauthenticated(): в if request.user.isauthenticated():В этом случае, требуется меньше набора текста - и это хорошо!В нашем случае, клиент и сервер будут работать в разных местах. Например, сервер будет напущен по адресу http://localhost:3000, а клиент по адресу http://localhost:5000. Браузер будет считать, что эти две локации будут находиться в разных местах, аналогично запуску сервера на http://www.server.com и клиента на http://www.clent.com. Мы не будем разрешать внешним доменам получать доступ к нашим файлам cookie, поэтому нам нужно найти другое, альтернативное решение, для использования сессий.Если вам интересно, почему мы не разрешаем доступ к нашим файлам cookie, ознакомьтесь со статьями о совместном использовании ресурсов между источниками (Cross-Origin Resource Sharing, CORS) и подделке межсайтовых запросов (Cross-Site Request Forgery, CSRF), по ссылкам ниже:CORSCSRFАутентификация, основанная на токенахНаиболее распространенной альтернативой аутентификации на основе сессий/сеансов является т.н. аутентификация на основе токенов. Мы будем использовать особую форму такой аутентификации для защиты нашего приложения. При аутентификации на основе токенов сервер предоставляет клиенту токен после успешного запроса на вход. Этот токен уникален для пользователя и хранится в базе данных вместе идентификатором пользователя (если точнее, возможны раные вариации генерации токена, основная же идея в том, чтобы он аутентифицировал пользователя, позволяя знать кто это и давая доступ к апи, и имел время жизни, по истечении которого "протухал"). Ожидается, что клиент отправит токен вместе с будущими запросами, чтобы сервер мог идентифицировать пользователя. Сервер делает это путем поиска в таблице базы данных, содержащей все созданные токены. Если соответствующий токен найден, то сервер продолжает проверять, действителен ли токен. Если не найден, мы говорим, что пользователь не аутентифицирован. Поскольку токены хранятся в базе данных, а не в файлах куки, аутентификация на основе токенов соответствует нашим потребностям.Верификация токеновМы всегда имеем возможность сохранить не только идентификатор пользователя (ID) с его токеном. Мы также можем хранить такие вещи, как дата истечения срока действия токена. В данном примере нам необходимо убедиться, что срок действия токена не прошел. Если прошел - считать, что токен недействителен. В таком случае, мы удаляем его из базы данных и просим пользователя снова войти в систему.JSON Web TokensJSON Web Token (сокр. JWT) - это открытый стандарт (RFC 7519) , который определяет компактный и автономный способ безопасной передачи информации между двумя сторонами. Можно думать о JWT как о токенах аутентификации на стероидах.Помните, что мы сказали, что будем использовать особую форму аутентификации на основе токенов? JWT это как раз то, что имелось ввиду.Почему JSON Web Tokes лучше обычных токенов?При переходе с обычных токенов на JWT мы получаем несколько преимуществ:
- JWT - открытый стандарт. Это означает, что что все реализации JWT должны быть довольно похожими, что является преимуществом при работе с разными языками и технологиями. Обычные токены имеют более свободную форму, что позволяет разработчику решать, как лучше всего реализовывать токены.
- JWT содержат информацию о пользователе, что удобно для клиентской стороны.
- Библиотеки здесь берут на себя основную тяжелую работу. Развертывание собственной системы аутентификации опасно, поэтому мы оставляем важные вещи проверенным «в боях» библиотекам, которым можем доверять.
Создание приложения, создание пользовательской моделиДля начала, создадим проект. Перейдите в терминале в вашу рабочую директорию, и выполните командуdjango-admin startproject json_auth_project (если вылезает ошибка, установите глобально джангу командой pip3 install django).Теперь необходимо создать виртуальное окружение. Перейдите в директорию проекта командой cd jsonauthproject, далее выполните команду python3 -m venv venv. Виртуальное окружение может создаваться некоторое время на слабых компьютерах, но итогом станет появление директории venv, содержащей виртуальное окружение. Его необходимо активировать, для этого выполните команду . ./venv/bin/activate. Далее советую создать файл requirements.txt, который по мере наполнения проекта внешними пакетами актуализировать (так же обязательно установите в окружении пакет django командой pip3 install django). Выполните команду pip3 freeze > requirements.txt (выполняйте данную команду каждый раз, когда добавляете новый пакет/ы в проект). Советую сразу применить стандартные системные миграции командой ./manage.py migrate. Теперь можно попробовать запустить проект, чтобы убедиться, что все работает. Для запуска девелоп-сервера выполните команду ./manage.py runserver. Результатом станут запуск сервера на localhost и портом по умолчанию 8000. Перейдите в браузере по ссылке http://localhost:8000. Видите ракету с надписью "The install worked successfully! Congratulations!" - она готова к запуску :)Для начала, создадим апп (app) authentication: ./manage.py startapp authentication. В файле apps/authentication/models.py будут храниться модели, которые мы будем использовать для аутентификации. Создайте этот файл, если его нет.Нам понадобится следующий набор импортов для создания классов User и UserManager, поэтому добавьте в начало файла код:
import jwt
from datetime import datetime, timedelta
from django.conf import settings from django.contrib.auth.models import (
AbstractBaseUser, BaseUserManager, PermissionsMixin
)
from django.db import models
При настройке аутентификации в Django одним из требований является указание настраиваемого класса Manager с двумя методами: createuser() и createsuperuser(). Чтобы узнать больше о пользовательской аутентификации в Django, прочтите https://docs.djangoproject.com/en/3.1/topics/auth/customizing/#substituting-a-custom-user-modelНаберите следующий код класса UserManager в файл apps/authentication/models.py и обязательно примите к сведению комментарии (больше про менеджеров: https://docs.djangoproject.com/en/3.1/topics/db/managers/):
class UserManager(BaseUserManager):
"""
Django требует, чтобы кастомные пользователи определяли свой собственный
класс Manager. Унаследовавшись от BaseUserManager, мы получаем много того
же самого кода, который Django использовал для создания User (для демонстрации).
"""
def create_user(self, username, email, password=None):
""" Создает и возвращает пользователя с имэйлом, паролем и именем. """
if username is None:
raise TypeError('Users must have a username.')
if email is None:
raise TypeError('Users must have an email address.')
user = self.model(username=username, email=self.normalize_email(email))
user.set_password(password)
user.save()
return user
def create_superuser(self, username, email, password):
""" Создает и возввращет пользователя с привилегиями суперадмина. """
if password is None:
raise TypeError('Superusers must have a password.')
user = self.create_user(username, email, password)
user.is_superuser = True
user.is_staff = True
user.save()
return user
Теперь, когда мы имеем класс менеджера, мы можем создать модель пользователя, наберите далее:
class User(AbstractBaseUser, PermissionsMixin):
# Каждому пользователю нужен понятный человеку уникальный идентификатор,
# который мы можем использовать для предоставления User в пользовательском
# интерфейсе. Мы так же проиндексируем этот столбец в базе данных для
# повышения скорости поиска в дальнейшем.
username = models.CharField(db_index=True, max_length=255, unique=True)
# Так же мы нуждаемся в поле, с помощью которого будем иметь возможность
# связаться с пользователем и идентифицировать его при входе в систему.
# Поскольку адрес почты нам нужен в любом случае, мы также будем
# использовать его для входы в систему, так как это наиболее
# распространенная форма учетных данных на данный момент (ну еще телефон).
email = models.EmailField(db_index=True, unique=True)
# Когда пользователь более не желает пользоваться нашей системой, он может
# захотеть удалить свой аккаунт. Для нас это проблема, так как собираемые
# нами данные очень ценны, и мы не хотим их удалять :) Мы просто предложим
# пользователям способ деактивировать учетку вместо ее полного удаления.
# Таким образом, они не будут отображаться на сайте, но мы все еще сможем
# далее анализировать информацию.
is_active = models.BooleanField(default=True)
# Этот флаг определяет, кто может войти в административную часть нашего
# сайта. Для большинства пользователей это флаг будет ложным.
is_staff = models.BooleanField(default=False)
# Временная метка создания объекта.
created_at = models.DateTimeField(auto_now_add=True)
# Временная метка показывающая время последнего обновления объекта.
updated_at = models.DateTimeField(auto_now=True)
# Дополнительный поля, необходимые Django
# при указании кастомной модели пользователя.
# Свойство USERNAME_FIELD сообщает нам, какое поле мы будем использовать
# для входа в систему. В данном случае мы хотим использовать почту.
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']
# Сообщает Django, что определенный выше класс UserManager
# должен управлять объектами этого типа.
objects = UserManager()
def __str__(self):
""" Строковое представление модели (отображается в консоли) """
return self.email
@property
def token(self):
"""
Позволяет получить токен пользователя путем вызова user.token, вместо
user._generate_jwt_token(). Декоратор @property выше делает это
возможным. token называется "динамическим свойством".
"""
return self._generate_jwt_token()
def get_full_name(self):
"""
Этот метод требуется Django для таких вещей, как обработка электронной
почты. Обычно это имя фамилия пользователя, но поскольку мы не
используем их, будем возвращать username.
"""
return self.username
def get_short_name(self):
""" Аналогично методу get_full_name(). """
return self.username
def _generate_jwt_token(self):
"""
Генерирует веб-токен JSON, в котором хранится идентификатор этого
пользователя, срок действия токена составляет 1 день от создания
"""
dt = datetime.now() + timedelta(days=1)
token = jwt.encode({
'id': self.pk,
'exp': int(dt.strftime('%s'))
}, settings.SECRET_KEY, algorithm='HS256')
return token.decode('utf-8')
Если хотите узнать немного больше о кастомной аутентификации пользователя, несколько ссылок для глубокого изучения:
- models.CustomUser - охватывает все, что Django ожидает от кастомной модели User https://docs.djangoproject.com/en/3.1/topics/auth/customizing/#django.contrib.auth.models.CustomUser
- models.AbstractBaseUser и models.PermissionsMixin - предоставляют несколько требований выше сразу https://docs.djangoproject.com/en/3.1/topics/auth/customizing/#django.contrib.auth.models.AbstractBaseUser https://docs.djangoproject.com/en/3.1/topics/auth/customizing/#django.contrib.auth.models.PermissionsMixin
- models.BaseUserManager - дает нам несколько полезных инструментов для запуска нашего класса UserManager https://docs.djangoproject.com/en/3.1/topics/auth/customizing/#django.contrib.auth.models.BaseUserManager
- В справочнике по полям модели перечислены различные типы полей, поддерживаемые Django, и параметры, которые принимает каждое поле (например, db_index и unique) https://docs.djangoproject.com/en/3.1/ref/models/fields/
Определение AUTH_USER_MODEL в настройках проектаПо-умолчанию, Django предполагает, что модель пользователя стандартная - django.contrib.auth.models.User. Однако, мы хотим в качестве модели пользователя использовать нашу созданную модель. Поскольку мы создали класс User, следующее что нам нужно сделать, это указать Django использовать нашу модель User, а не стандартную.Потратьте немного времени и почитайте о замене стандартной модели пользователя в Django: https://docs.djangoproject.com/en/3.1/topics/auth/customizing/#substituting-a-custom-user-modelЕсли вы уже перенесли свою модель базы данных до того, как указали кастомную модель User, вам может потребоваться удалить свою базу данных и повторно запустить миграции.Укажите Django на использование нашей модели User, указав параметр AUTH_USER_MODEL в файле project/settings.py. Чтобы установить кастомную модель, введите в нижней части файла project/settings.py:
# Рассказать Django о созданной нами кастомной модели пользователя. Строка
# authentication.User сообщает Django, что мы ссылаемся на модель User в модуле
# authentication. Этот модуль зарегистрирован выше в настройке INSTALLED_APPS.
AUTH_USER_MODEL = 'authentication.User'
Создание и запуск миграцийПо мере добавления новых моделей и изменения существующих, нам потребуется обновлять базу данных, чтобы отобразить эти изменения. Миграции - это то, что Django использует, чтобы сообщить базе данных, что что-то изменилось. Наша же миграция сообщит, что нам нужно добавить новую таблицу для нашей кастомной модели User.
- Примечание: Если вы уже использовали ./manage.py makemigrations или ./manage.py migrate, вам необходимо удалить базу данных прежде, чем продолжать. Для SQLite достаточно просто удалить файл, лежащий в корне директории проекта. Django будет недоволен, если вы измените AUTH_USER_MODEL после создания базы данных, и лучше всего просто удалить базу данных и начать заново.
Теперь мы готовы создавать и применять миграции. После этого мы сможем создать нашего первого пользователя. Чтобы создать миграцию, необходимо запустить в консоли следующую команду:./manage.py makemigrationsЭто создаст стандартные миграции для нашего нового проекта Django. Однако, это не создат миграции для новых приложений внутри нашего проекта. В первый раз, когда мы хотим создать миграции для нового приложения, мы должны быть более конкретны.Чтобы создать миграции для приложения authenticate, выполните./manage.py makemigrations authenticationЭто создаст инициализирующую миграцию для приложения authentication. В будущем. когда вы захотите сгенерировать новый миграции для приложения аутентификации, вам нужно будет запустить только./manage.py makemigrationsТеперь мы можем применить миграции с помощью команды:./manage.py migrateВ отличие от makemigrations, вам не нужно указывать название приложения при выполнении migrate.Наш первый пользовательИтак, мы создали нашу модель User, более того, наша база данных поднята и работает. Следующим шагом будет создание первого объекта пользователя, User. Мы сделаем этого пользователя суперадмином, так как будем использовать его для дальнейшего тестирования нашего приложения.Создайте пользователя с помощью следующей команды терминала:./manage.py createsuperuserDjango спросит вас о параметрах нового пользователя - почте, никнейме и пароле. После ввода данных, пользователь будет создан. Мои поздравления! :)Для проверки успешного создания пользователя, перейдите в шелл Django, посредством выполнения следующей команды:./manage.py shell_plus (или стандартную версию шелла ./manage.py shell)Оболочка shell_plus предоставляется библиотекой django-extensions, которую необходимо установить (pip3 install django-extensions), если вы хотите пользоваться shell_plus. Это удобно, так как он автоматически импортирует модели всех приложения, указанных в INSTALLED_APPS. При желании, его также можно настроить для автоматического импорта других утилит.После открытия оболочки, выполните следующие команды:
user = User.objects.first()
user.username
user.token
Если все сделано нормально, вы должны увидеть в выводах username и token.Регистрация новых пользователейНа текущий момент, пользователь не может делать чего бы то ни было интересного. Нашей следующей задачей будет создание эндпоинта для регистрации новых пользователей.RegistrationSerializerСоздайте файл apps/authentication/serializers.py и наберите туда следующий код:
from rest_framework import serializers
from .models import User
class RegistrationSerializer(serializers.ModelSerializer):
""" Сериализация регистрации пользователя и создания нового. """
# Убедитесь, что пароль содержит не менее 8 символов, не более 128,
# и так же что он не может быть прочитан клиентской стороной
password = serializers.CharField(
max_length=128,
min_length=8,
write_only=True
)
# Клиентская сторона не должна иметь возможность отправлять токен вместе с
# запросом на регистрацию. Сделаем его доступным только на чтение.
token = serializers.CharField(max_length=255, read_only=True)
class Meta:
model = User
# Перечислить все поля, которые могут быть включены в запрос
# или ответ, включая поля, явно указанные выше.
fields = ['email', 'username', 'password', 'token']
def create(self, validated_data):
# Использовать метод create_user, который мы
# написали ранее, для создания нового пользователя.
return User.objects.create_user(**validated_data)
Прочитайте внимательно код, обращая особое внимание на комментарии, а затем продолжим.Немного о ModelSerializerВ приведенном выше коде мы создали класс RegistrationSerializer, который наследуется от сериализатора serializers.ModelSerializer. serializers.ModelSerializer - это просто абстракция поверх serializers.Serializer, про которую подробней можно почитать в документации Django REST Framework (DFR). ModelSerializer просто напросто упрощает для нас выполнение некоторых стандартных вещей, относящихся к сериализации моделей Django. Следует также отметить, что он позволяет указать два метода: создание и обновление. В приведенном выше примере мы написали наш собственный метод create() с использованием User.objects.create_user(), но не указали метод обновления. В этом случае DRF будет использовать собственный метод обновления по умолчанию для обновления пользователя.RegistrationAPIViewТеперь мы можем сериализовывать запросы и ответы для регистрации пользователя. Далее, мы должны создать вью (views) для реализации эндпоинта (endpoint), что даст клиентской стороне URL для запроса на создание нового пользователя.Создайте файл apps/authentication/views.py если его нет и наберите следующий код:
from rest_framework import status
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView
from .serializers import RegistrationSerializer
class RegistrationAPIView(APIView):
"""
Разрешить всем пользователям (аутентифицированным и нет) доступ к данному эндпоинту.
"""
permission_classes = (AllowAny,)
serializer_class = RegistrationSerializer
def post(self, request):
user = request.data.get('user', {})
# Паттерн создания сериализатора, валидации и сохранения - довольно
# стандартный, и его можно часто увидеть в реальных проектах.
serializer = self.serializer_class(data=user)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
Проговорим о паре новых вещей в коде выше:
- Свойство permission_classes - это то, что решает, кто может использовать этот эндпоинт. Мы можем ограничить это авторизованными пользователями или администраторами и т.д. и т.п.
- Паттерн создания сериализатора, валидации и сохранения, который можно увидеть в методе post - довольно стандартный, и его можно часто увидеть в реальных проектах. Ознакомьтесь с ним подробнее.
Почитать про Django REST Framework (DRF) Permissions можно по ссылке https://www.django-rest-framework.org/api-guide/permissions/Теперь мы должны настроить маршрутизацию для нашего проекта. В Django 1.x => 2.x были внесены некоторые изменения при переключении с URL на путь (path). Есть довольно много вопросов по адаптации старого URL-адреса к новому способу определения URL's, но это выходит далеко за рамки данной статьи. Стоит сказать, что в последних версиях Django работа с роутами значительно упростилась, в чем можно убедиться далее.Создайте файл apps/authentication/urls.py и поместите в него следующий код для обработки маршрутов нашего приложения:
from django.urls import path
from .views import RegistrationAPIView
app_name = 'authentication'
urlpatterns = [
path('users/', RegistrationAPIView.as_view()),
]
В Django настоятельно рекомендуется создавать пути для конкретных модульных приложений. Это по факту заставляет задумываться о дизайне приложения и сохранении его автономности и возможности повторного использования. Что в данном случае мы и сделали. Мы также указали app_name = 'authentication', чтобы мы могли использовать включение (including) и придерживаться модульности приложения. Теперь нужно включить указанный выше файл в наш файл глобальных URL-адресов.Откройте project/urls.py и вы увидите следующую строку в верхней части файла:from django.urls import pathПервое, что нужно сделать, это импортировать метод include() из django.urlsfrom django.urls import path, includeМетод include() позволяет включить еще один файл без необходимости выполнения кучи другой работы, например, импортирования а затем повторной регистрации маршрута в этом файле.Ниже видим следующее:
urlpatterns = [
path('admin/', admin.site.urls),
]
Обновим это, чтобы включить наш новый файл urls.py:
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('apps.authentication.urls', namespace='authentication')),
]
Регистрация пользователя с помощью PostmanТеперь, когда мы создали модель User и добавили эндпоинт для регистрации новых пользователей, выполним быструю проверку работоспособности, чтобы убедиться, что мы на правильном пути. Для этого использует прекрасный инструмент тестирования (и далеко не только) апи под названием Postman (ознакомиться с его функциональность подробнее можно по ссылке https://learning.postman.com/docs/getting-started/introduction/).Необходимо сформировать POST запрос по пути localhost:8000/api/users/ со следующей структурой данных в теле запроса:
{
"user": {
"username": "user1",
"email": "user1@user.user",
"password": "qweasdzxc"
}
}
В ответ вернутся данных только что созданного пользователя. Мои поздравления! Все работает как надо, правда, с небольшим нюансом. В ответе вся информация пользователя находится на корневом уровне, а не располагается в поле "user". Чтобы это исправить (чтобы ответ был похож на тело запроса при регистрации, указанное выше), нужно создать настраиваемое средство визуализации DRF (renderer).Рендеринг объектов UserСоздайте файл под названием apps/authentication/renderers.py и наберите в него следующий код:
import json
from rest_framework.renderers import JSONRenderer
class UserJSONRenderer(JSONRenderer):
charset = 'utf-8'
def render(self, data, media_type=None, renderer_context=None):
# Если мы получим ключ token как часть ответа, это будет байтовый
# объект. Байтовые объекты плохо сериализуются, поэтому нам нужно
# декодировать их перед рендерингом объекта User.
token = data.get('token', None)
if token is not None and isinstance(token, bytes):
# Как говорится выше, декодирует token если он имеет тип bytes.
data['token'] = token.decode('utf-8')
# Наконец, мы можем отобразить наши данные в простанстве имен 'user'.
return json.dumps({
'user': data
})
Здесь ничего особо нового или интересного не происходит, поэтому прочитайте комментарии в коде и двигаемся дальше.Теперь, откройте файл apps/auhentication/views.py и импортируйте созданный нами UserJSONRenderer, добавив следующую строку:from .renderers import UserJSONRendererКроме того, необходимо установить свойство renderer_classes класса RegistrationAPIView:
renderer_classes = (UserJSONRenderer,)
Теперь, имея UserJSONRenderer на нужном месте, используйте запрос в Postman'e на создание нового пользователя. Обратите внимание, что теперь ответ находится внутри пространства имен "user".Вход пользователей в системуПоскольку теперь пользователи могут зарегистрироваться в приложении, нам нужно реализовать для них способ входа в свою учетную запись. Далее, мы добавим сериализатор и представление, необходимые пользователям для входа в систему. Мы также начнем смотреть, как наш API должен обрабатывать ошибки.LoginSerializerОткройте файл apps/authentication/serializers.py и добавьте следующий импорт:
from django.contrib.auth import authenticate
После, наберите следующий код сериализатора в конце файла:
class LoginSerializer(serializers.Serializer):
email = serializers.CharField(max_length=255)
username = serializers.CharField(max_length=255, read_only=True)
password = serializers.CharField(max_length=128, write_only=True)
token = serializers.CharField(max_length=255, read_only=True)
def validate(self, data):
# В методе validate мы убеждаемся, что текущий экземпляр
# LoginSerializer значение valid. В случае входа пользователя в систему
# это означает подтверждение того, что присутствуют адрес электронной
# почты и то, что эта комбинация соответствует одному из пользователей.
email = data.get('email', None)
password = data.get('password', None)
# Вызвать исключение, если не предоставлена почта.
if email is None:
raise serializers.ValidationError(
'An email address is required to log in.'
)
# Вызвать исключение, если не предоставлен пароль.
if password is None:
raise serializers.ValidationError(
'A password is required to log in.'
)
# Метод authenticate предоставляется Django и выполняет проверку, что
# предоставленные почта и пароль соответствуют какому-то пользователю в
# нашей базе данных. Мы передаем email как username, так как в модели
# пользователя USERNAME_FIELD = email.
user = authenticate(username=email, password=password)
# Если пользователь с данными почтой/паролем не найден, то authenticate
# вернет None. Возбудить исключение в таком случае.
if user is None:
raise serializers.ValidationError(
'A user with this email and password was not found.'
)
# Django предоставляет флаг is_active для модели User. Его цель
# сообщить, был ли пользователь деактивирован или заблокирован.
# Проверить стоит, вызвать исключение в случае True.
if not user.is_active:
raise serializers.ValidationError(
'This user has been deactivated.'
)
# Метод validate должен возвращать словать проверенных данных. Это
# данные, которые передются в т.ч. в методы create и update.
return {
'email': user.email,
'username': user.username,
'token': user.token
}
Когда сериализатор будет на своем месте, можно отправляться писать представление.LoginAPIViewОткройте файл apps/authentication/views.py и обновите импорты:
from .serializers import LoginSerializer, RegistrationSerializer
А затем, добавьте само представление:
class LoginAPIView(APIView):
permission_classes = (AllowAny,)
renderer_classes = (UserJSONRenderer,)
serializer_class = LoginSerializer
def post(self, request):
user = request.data.get('user', {})
# Обратите внимание, что мы не вызываем метод save() сериализатора, как
# делали это для регистрации. Дело в том, что в данном случае нам
# нечего сохранять. Вместо этого, метод validate() делает все нужное.
serializer = self.serializer_class(data=user)
serializer.is_valid(raise_exception=True)
return Response(serializer.data, status=status.HTTP_200_OK)
Далее, откройте файл apps/authentication/urls.py и обновите импорт:
from .views import LoginAPIView, RegistrationAPIView
И затем добавьте новое правило в urlpatterns:
urlpatterns = [
path('users/', RegistrationAPIView.as_view()),
path('users/login/', LoginAPIView.as_view()),
]
Вход пользователя с помощью PostmanНа данном этапе, пользователь должен иметь возможность войти в систему, используя соответствующий эндпоинт нашей системы. Сделаем это :) Откроем использованный ранее Postman, и попробуем выполнить пост запрос на http://localhost:8000/api/users/login/, передав в теле почту и пароль ранее созданного пользователя. Если все было сделано верно, должен вернуться объект вида:
{
"user": {
"email": "email@email.email",
"username": "admin",
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjA1MTE3MjkwfQ.W8B6RY-jGO9PYDTzDWxhrkSHsTe1p3jlzq1BL7Tbwcs"
}
}
Как видно, в ответ включен token, который можно использовать для выполнения всех будущих запросов, требующих аутентификации пользователя.Есть еще кое-что, что нам необходимо сделать. Попробуйте выполнить вход, указав неверные почту или пароль, или оба. Обратите внимание на ответ с ошибкой - с ней есть две проблемы. Во-первых, non_field_errors выглядит странно. Обычно этот ключ должен обзываться именем поля, по которому сериализатор не прошел проверку. Поскольку мы переопределили весь метод проверки, вместо использования отдельных методов, зависящих от проверяемого поля, такого как например validate_email, Django REST Framework не знает, какое поле атрибутировать. По умолчанию, это поле обзывается nonfield_errors, и так как наш клиент будет использовать это поле для отображения ошибок, нам необходимо изменить такое поведение. Во-вторых, клиент будет ожидать, что любые ошибки будут помещены пространство имен под соответствующим ключом ошибки в JSON ответе (как мы сделали это в эндпоинтах регистрации и входа). Мы добьемся этого, переопределив стандартную обработку ошибок Django REST Framework.Перегрузка EXCEPTION_HANDLER и NON_FIELD_ERRORS_KEYОдна из настроек DRF под названием EXCEPTION_HANDLER возвращает словарь ошибок. Мы хотим, чтобы имена наших ошибок находились под общим единым ключом, потому нам нужно переопределить EXCEPTION_HANDLER. Так же переопределим и NON_FIELD_ERRORS_KEY, как упоминалось ранее.Начнем с создания project/exceptions.py, и добавления в него следующего кода:
from rest_framework.views import exception_handler
def core_exception_handler(exc, context):
# Если возникает исключение, которые мы не обрабатываем здесь явно, мы
# хотим передать его обработчику исключений по-умолчанию, предлагаемому
# DRF. И все же, если мы обрабатываем такой тип исключения, нам нужен
# доступ к сгенерированному DRF - получим его заранее здесь.
response = exception_handler(exc, context)
handlers = {
'ValidationError': _handle_generic_error
}
# Определить тип текущего исключения. Мы воспользуемся этим сразу далее,
# чтобы решить, делать ли это самостоятельно или отдать эту работу DRF.
exception_class = exc.__class__.__name__
if exception_class in handlers:
# Если это исключение можно обработать - обработать :) В противном
# случае, вернуть ответ сгенерированный стандартными средствами заранее
return handlers[exception_class](exc, context, response)
return response
def _handle_generic_error(exc, context, response):
# Это самый простой обработчик исключений, который мы можем создать. Мы
# берем ответ сгенерированный DRF и заключаем его в ключ 'errors'.
response.data = {
'errors': response.data
}
return response
Позаботившись об этом, откройте файл project/settings.py и добавьте новый параметр под названием REST_FRAMEWORK в конец файла:
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'project.exceptions.core_exception_handler',
'NON_FIELD_ERRORS_KEY': 'error',
}
Так переопределяются стандартные настройки DFR. Чуть позже, мы добавим еще одну настройку, когда будем писать представления, требующие аутентификации пользователя. Попробуйте отправить еще один некорректный (с неверными почтой и/или паролем) запрос на вход с помощью Postman - сообщение об ошибке должно измениться.Обновить UserJSONRendererНет, в ответе полученном с неверными почтой/паролем все совсем не так, как ожидалось. Да, мы получили ключ "error", но все пространство имен заключено в ключе "user", что совсем не хорошо. Давайте обновим UserJSONRenderer чтобы проверять ключ "error" и предпринять некоторые действия в таком случае. Откройте файл apps/authenticate/renderers.py и внесите следующие изменения:
import json
from rest_framework.renderers import JSONRenderer
class UserJSONRenderer(JSONRenderer):
charset = 'utf-8'
def render(self, data, media_type=None, renderer_context=None):
# Если представление выдает ошибку (например, пользователь не может
# быть аутентифицирован), data будет содержать ключ error. Мы хотим,
# чтобы стандартный JSONRenderer обрабатывал такие ошибки, поэтому
# такой случай необходимо проверить.
errors = data.get('errors', None)
# Если мы получим ключ token как часть ответа, это будет байтовый
# объект. Байтовые объекты плохо сериализуются, поэтому нам нужно
# декодировать их перед рендерингом объекта User.
token = data.get('token', None)
if errors is not None:
# Позволим стандартному JSONRenderer обрабатывать ошибку.
return super(UserJSONRenderer, self).render(data)
if token is not None and isinstance(token, bytes):
# Как говорится выше, декодирует token если он имеет тип bytes.
data['token'] = token.decode('utf-8')
# Наконец, мы можем отобразить наши данные в простанстве имен 'user'.
return json.dumps({
'user': data
})
Теперь, пошлите снова некорректный (неверные почта/пароль) запрос с помощью Postman - все должно быть как ожидается.Получение и обновление пользователей.Пользователи могут регистрировать новые аккаунты и заходить, логиниться в эти аккаунты. Теперь пользователи нуждаются в возможности получить и обновить свою информацию. Давайте реализуем это прежде чем перейти к созданию профилей пользователей.UserSerializerМы собираемся создать еще один сериализатор для профилей. У нас есть сериализаторы для запросов входа и регистрации, но нам так же нужна возможность сериализации самих пользовательских объектов.Откройте файл apps/authentication/serializers.py и добавьте следующий код:
class UserSerializer(serializers.ModelSerializer):
""" Ощуществляет сериализацию и десериализацию объектов User. """
# Пароль должен содержать от 8 до 128 символов. Это стандартное правило. Мы
# могли бы переопределить это по-своему, но это создаст лишнюю работу для
# нас, не добавляя реальных преимуществ, потому оставим все как есть.
password = serializers.CharField(
max_length=128,
min_length=8,
write_only=True
)
class Meta:
model = User
fields = ('email', 'username', 'password', 'token',)
# Параметр read_only_fields является альтернативой явному указанию поля
# с помощью read_only = True, как мы это делали для пароля выше.
# Причина, по которой мы хотим использовать здесь 'read_only_fields'
# состоит в том, что нам не нужно ничего указывать о поле. В поле
# пароля требуются свойства min_length и max_length,
# но это не относится к полю токена.
read_only_fields = ('token',)
def update(self, instance, validated_data):
""" Выполняет обновление User. """
# В отличие от других полей, пароли не следует обрабатывать с помощью
# setattr. Django предоставляет функцию, которая обрабатывает пароли
# хешированием и 'солением'. Это означает, что нам нужно удалить поле
# пароля из словаря 'validated_data' перед его использованием далее.
password = validated_data.pop('password', None)
for key, value in validated_data.items():
# Для ключей, оставшихся в validated_data мы устанавливаем значения
# в текущий экземпляр User по одному.
setattr(instance, key, value)
if password is not None:
# 'set_password()' решает все вопросы, связанные с безопасностью
# при обновлении пароля, потому нам не нужно беспокоиться об этом.
instance.set_password(password)
# После того, как все было обновлено, мы должны сохранить наш экземпляр
# User. Стоит отметить, что set_password() не сохраняет модель.
instance.save()
return instance
Стоит отметить, что мы не определяем явно метод create в данном сериализаторе, поскольку DRF предоставляет метод создания по умолчанию для всех экземпляров serializers.ModelSerializer. С помощью этого сериализатора можно создать пользователя, но мы хотим, чтобы создание пользователя производилось с помощью RegistrationSerializer.UserRetrieveUpdateAPIViewОткройте файл apps/authentication/views.py и обновите импорты следующим образом:
from rest_framework import status
from rest_framework.generics import RetrieveUpdateAPIView
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from .renderers import UserJSONRenderer
from .serializers import (
LoginSerializer, RegistrationSerializer, UserSerializer,
)
Прописав импорты, создайте новое представление под названием UserRetrieveUpdateView:
class UserRetrieveUpdateAPIView(RetrieveUpdateAPIView):
permission_classes = (IsAuthenticated,)
renderer_classes = (UserJSONRenderer,)
serializer_class = UserSerializer
def retrieve(self, request, *args, **kwargs):
# Здесь нечего валидировать или сохранять. Мы просто хотим, чтобы
# сериализатор обрабатывал преобразования объекта User во что-то, что
# можно привести к json и вернуть клиенту.
serializer = self.serializer_class(request.user)
return Response(serializer.data, status=status.HTTP_200_OK)
def update(self, request, *args, **kwargs):
serializer_data = request.data.get('user', {})
# Паттерн сериализации, валидирования и сохранения - то, о чем говорили
serializer = self.serializer_class(
request.user, data=serializer_data, partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
Теперь перейдем к файлу apps/authentication/urls.py и обновим импорты в начале файла, чтобы включить UserRetrieveUpdateView:
from .views import (
LoginAPIView, RegistrationAPIView, UserRetrieveUpdateAPIView
)
И добавим новый путь в urlpatterns:
urlpatterns = [
path('user', UserRetrieveUpdateAPIView.as_view()),
path('users/', RegistrationAPIView.as_view()),
path('users/login/', LoginAPIView.as_view()),
]
Откройте Postman и отправьте запрос на текущего пользователя на получение информации о текущем пользователе (GET localhost:8000/api/user/). Если все сделано правильно, вы должны получить сообщение об ошибке как следующее:
{
"user": {
"detail": "Authentication credentials were not provided."
}
}
Аутентификация пользователейВ Django существует идея бекендов аутентификации. Не вдаваясь в подробности, бекенд - это, по сути, план принятия решения о том, аутентифицирован ли пользователь. Нам нужно создать собственный бекенд для поддержки JWT, поскольку по умолчанию он не поддерживается ни Django, ни Django REST Framework (DRF).Создайте и откройте файл apps/authentication/backends.py и добавьте в него следующий код:
import jwt
from django.conf import settings
from rest_framework import authentication, exceptions
from .models import User
class JWTAuthentication(authentication.BaseAuthentication):
authentication_header_prefix = 'Token'
def authenticate(self, request):
"""
Метод authenticate вызывается каждый раз, независимо от того, требует
ли того эндпоинт аутентификации. 'authenticate' имеет два возможных
возвращаемых значения:
1) None - мы возвращаем None если не хотим аутентифицироваться.
Обычно это означает, что мы значем, что аутентификация не удастся.
Примером этого является, например, случай, когда токен не включен в
заголовок.
2) (user, token) - мы возвращаем комбинацию пользователь/токен
тогда, когда аутентификация пройдена успешно. Если ни один из
случаев не соблюден, это означает, что произошла ошибка, и мы
ничего не возвращаем. В таком случае мы просто вызовем исключение
AuthenticationFailed и позволим DRF сделать все остальное.
"""
request.user = None
# 'auth_header' должен быть массивом с двумя элементами:
# 1) именем заголовка аутентификации (Token в нашем случае)
# 2) сам JWT, по которому мы должны пройти аутентифкацию
auth_header = authentication.get_authorization_header(request).split()
auth_header_prefix = self.authentication_header_prefix.lower()
if not auth_header:
return None
if len(auth_header) == 1:
# Некорректный заголовок токена, в заголовке передан один элемент
return None
elif len(auth_header) > 2:
# Некорректный заголовок токена, какие-то лишние пробельные символы
return None
# JWT библиотека которую мы используем, обычно некорректно обрабатывает
# тип bytes, который обычно используется стандартными библиотеками
# Python3 (HINT: использовать PyJWT). Чтобы точно решить это, нам нужно
# декодировать prefix и token. Это не самый чистый код, но это хорошее
# решение, потому что возможна ошибка, не сделай мы этого.
prefix = auth_header[0].decode('utf-8')
token = auth_header[1].decode('utf-8')
if prefix.lower() != auth_header_prefix:
# Префикс заголовка не тот, который мы ожидали - отказ.
return None
# К настоящему моменту есть "шанс", что аутентификация пройдет успешно.
# Мы делегируем фактическую аутентификацию учетных данных методу ниже.
return self._authenticate_credentials(request, token)
def _authenticate_credentials(self, request, token):
"""
Попытка аутентификации с предоставленными данными. Если успешно -
вернуть пользователя и токен, иначе - сгенерировать исключение.
"""
try:
payload = jwt.decode(token, settings.SECRET_KEY)
except Exception:
msg = 'Ошибка аутентификации. Невозможно декодировать токеню'
raise exceptions.AuthenticationFailed(msg)
try:
user = User.objects.get(pk=payload['id'])
except User.DoesNotExist:
msg = 'Пользователь соответствующий данному токену не найден.'
raise exceptions.AuthenticationFailed(msg)
if not user.is_active:
msg = 'Данный пользователь деактивирован.'
raise exceptions.AuthenticationFailed(msg)
return (user, token)
В этом файле много логики и исключений, но код довольно прост. Все, что мы сделали, это составили список условий, которые пользователю необходимо пройти для аутентификации, и описали исключения, которые выбросятся, если какое-то из условий окажется истинным.Сообщить DRF про наш аутентификационный бекендМы должны явно указать Django REST Framework, какой бекенд аутентификации мы хотим использовать, аналогично тому, как мы сказали Django использовать нашу пользовательскую модель.Откройте файл project/settings.py и обновите словарь REST_FRAMEWORK следующим новым ключом:
REST_FRAMEWORK = {
...
'DEFAULT_AUTHENTICATION_CLASSES': (
'apps.authentication.backends.JWTAuthentication',
),
}
Получение и обновление пользователей с помощью PostmanТеперь, когда наш новый бекенд аутентификаци установлен, ошибки аутентификации, которые мы наблюдали ранее, должна исчезнуть. Проверьте это, открыв Postman и отправив тот же самый запрос (GET localhost:8000/api/user/). Он должен быть успешным, и вы должны увидеть информацию о своем пользователе в ответе приложения. Помните, что мы создали эндпоинт обновления вместе с эндпоинтом получения информации? Давайте проверим и это. Отправьте запрос на обновление почты пользователя (PATCH localhost:8000/api/user/), передав в заголовках запроса токен. Если все было сделано верно, в ответе вы увидите, как адрес электронной почты изменился.ИтогиПодведем краткие итоги того, что мы сделали в данной статье. Мы создали гибкую модель пользователя (в дальнейшем, можно ее расширять как душе угодно, добавляя разные поля, дополнительные модели и т.п.), три сериализатора, каждый из которых выполняют свою четко определенную функцию. Создали четыре эндпоинта, которые позволяют пользователям регистрироваться, входить в систему, получать и обновлять информацию о своем аккаунте. На мой взгляд, это очень приятный фундамент, основа, на которой можно строить какой-то новый ламповый проект по своему усмотрению:) (однако же, есть куча сфер, о которых можно говорить часами, например на языке крутится следующий этап о том, чтобы завернуть это все в контейнеры докера, прикрутив постгрес, редис и селери).
===========
Источник:
habr.com
===========
Похожие новости:
- [Python, Программирование, Обработка изображений, Управление медиа, Софт] Миллион домашних фотографий: наводим порядок
- [Python, Учебный процесс в IT, Дизайн игр] Стив пишет заклинания на Python. Обучение детей программированию в Minecraft
- [Python, API] Скрапинг Avito без headless-браузера
- [Python] Многопоточное скачивание файлов с ftp python-скриптом
- [Python, Машинное обучение, Контент-маркетинг, Искусственный интеллект, Социальные сети и сообщества] Нейросеть для раскрутки собачьего аккаунта в Инстаграм или робопёс в действии
- [Python, FPGA] Прокачиваем скрипты симуляции HDL с помощью Python и PyTest
- [Python, Машинное обучение, Искусственный интеллект] Распознавание Ворониных на фотографиях: от концепции к делу
- [Python, API, 1С-Битрикс] Как быстро получить много данных от Битрикс24 через REST API
- [Python, Программирование, Проектирование и рефакторинг, Профессиональная литература] Как определять собственные классы исключений в Python (перевод)
- [Python, Программирование, Искусственный интеллект] Constraint Programming или как решить задачу коммивояжёра, просто описав её (перевод)
Теги для поиска: #_python, #_django, #_python, #_django, #_jwt, #_auth, #_python, #_django
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 25-Ноя 11:45
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Данная статья является сборкой-компиляцией нескольких (основано на первой) статей, как результат моих изучений по теме jwt аутентификации в джанге со всем вытекающим. Так и не удалось (по крайней мере в рунете) найти нормальную статью, в которой рассказывается от этапа создания проекта, startproject, прикручивание jwt аутентификации.Добротно исследовав, отдаю на людской суд.Ссылки на пользуемые статьи прилагаются:
import jwt
from datetime import datetime, timedelta from django.conf import settings from django.contrib.auth.models import ( AbstractBaseUser, BaseUserManager, PermissionsMixin ) from django.db import models class UserManager(BaseUserManager):
""" Django требует, чтобы кастомные пользователи определяли свой собственный класс Manager. Унаследовавшись от BaseUserManager, мы получаем много того же самого кода, который Django использовал для создания User (для демонстрации). """ def create_user(self, username, email, password=None): """ Создает и возвращает пользователя с имэйлом, паролем и именем. """ if username is None: raise TypeError('Users must have a username.') if email is None: raise TypeError('Users must have an email address.') user = self.model(username=username, email=self.normalize_email(email)) user.set_password(password) user.save() return user def create_superuser(self, username, email, password): """ Создает и возввращет пользователя с привилегиями суперадмина. """ if password is None: raise TypeError('Superusers must have a password.') user = self.create_user(username, email, password) user.is_superuser = True user.is_staff = True user.save() return user class User(AbstractBaseUser, PermissionsMixin):
# Каждому пользователю нужен понятный человеку уникальный идентификатор, # который мы можем использовать для предоставления User в пользовательском # интерфейсе. Мы так же проиндексируем этот столбец в базе данных для # повышения скорости поиска в дальнейшем. username = models.CharField(db_index=True, max_length=255, unique=True) # Так же мы нуждаемся в поле, с помощью которого будем иметь возможность # связаться с пользователем и идентифицировать его при входе в систему. # Поскольку адрес почты нам нужен в любом случае, мы также будем # использовать его для входы в систему, так как это наиболее # распространенная форма учетных данных на данный момент (ну еще телефон). email = models.EmailField(db_index=True, unique=True) # Когда пользователь более не желает пользоваться нашей системой, он может # захотеть удалить свой аккаунт. Для нас это проблема, так как собираемые # нами данные очень ценны, и мы не хотим их удалять :) Мы просто предложим # пользователям способ деактивировать учетку вместо ее полного удаления. # Таким образом, они не будут отображаться на сайте, но мы все еще сможем # далее анализировать информацию. is_active = models.BooleanField(default=True) # Этот флаг определяет, кто может войти в административную часть нашего # сайта. Для большинства пользователей это флаг будет ложным. is_staff = models.BooleanField(default=False) # Временная метка создания объекта. created_at = models.DateTimeField(auto_now_add=True) # Временная метка показывающая время последнего обновления объекта. updated_at = models.DateTimeField(auto_now=True) # Дополнительный поля, необходимые Django # при указании кастомной модели пользователя. # Свойство USERNAME_FIELD сообщает нам, какое поле мы будем использовать # для входа в систему. В данном случае мы хотим использовать почту. USERNAME_FIELD = 'email' REQUIRED_FIELDS = ['username'] # Сообщает Django, что определенный выше класс UserManager # должен управлять объектами этого типа. objects = UserManager() def __str__(self): """ Строковое представление модели (отображается в консоли) """ return self.email @property def token(self): """ Позволяет получить токен пользователя путем вызова user.token, вместо user._generate_jwt_token(). Декоратор @property выше делает это возможным. token называется "динамическим свойством". """ return self._generate_jwt_token() def get_full_name(self): """ Этот метод требуется Django для таких вещей, как обработка электронной почты. Обычно это имя фамилия пользователя, но поскольку мы не используем их, будем возвращать username. """ return self.username def get_short_name(self): """ Аналогично методу get_full_name(). """ return self.username def _generate_jwt_token(self): """ Генерирует веб-токен JSON, в котором хранится идентификатор этого пользователя, срок действия токена составляет 1 день от создания """ dt = datetime.now() + timedelta(days=1) token = jwt.encode({ 'id': self.pk, 'exp': int(dt.strftime('%s')) }, settings.SECRET_KEY, algorithm='HS256') return token.decode('utf-8')
# Рассказать Django о созданной нами кастомной модели пользователя. Строка
# authentication.User сообщает Django, что мы ссылаемся на модель User в модуле # authentication. Этот модуль зарегистрирован выше в настройке INSTALLED_APPS. AUTH_USER_MODEL = 'authentication.User'
user = User.objects.first()
user.username user.token from rest_framework import serializers
from .models import User class RegistrationSerializer(serializers.ModelSerializer): """ Сериализация регистрации пользователя и создания нового. """ # Убедитесь, что пароль содержит не менее 8 символов, не более 128, # и так же что он не может быть прочитан клиентской стороной password = serializers.CharField( max_length=128, min_length=8, write_only=True ) # Клиентская сторона не должна иметь возможность отправлять токен вместе с # запросом на регистрацию. Сделаем его доступным только на чтение. token = serializers.CharField(max_length=255, read_only=True) class Meta: model = User # Перечислить все поля, которые могут быть включены в запрос # или ответ, включая поля, явно указанные выше. fields = ['email', 'username', 'password', 'token'] def create(self, validated_data): # Использовать метод create_user, который мы # написали ранее, для создания нового пользователя. return User.objects.create_user(**validated_data) from rest_framework import status
from rest_framework.permissions import AllowAny from rest_framework.response import Response from rest_framework.views import APIView from .serializers import RegistrationSerializer class RegistrationAPIView(APIView): """ Разрешить всем пользователям (аутентифицированным и нет) доступ к данному эндпоинту. """ permission_classes = (AllowAny,) serializer_class = RegistrationSerializer def post(self, request): user = request.data.get('user', {}) # Паттерн создания сериализатора, валидации и сохранения - довольно # стандартный, и его можно часто увидеть в реальных проектах. serializer = self.serializer_class(data=user) serializer.is_valid(raise_exception=True) serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED)
from django.urls import path
from .views import RegistrationAPIView app_name = 'authentication' urlpatterns = [ path('users/', RegistrationAPIView.as_view()), ] urlpatterns = [
path('admin/', admin.site.urls), ] urlpatterns = [
path('admin/', admin.site.urls), path('api/', include('apps.authentication.urls', namespace='authentication')), ] {
"user": { "username": "user1", "email": "user1@user.user", "password": "qweasdzxc" } } import json
from rest_framework.renderers import JSONRenderer class UserJSONRenderer(JSONRenderer): charset = 'utf-8' def render(self, data, media_type=None, renderer_context=None): # Если мы получим ключ token как часть ответа, это будет байтовый # объект. Байтовые объекты плохо сериализуются, поэтому нам нужно # декодировать их перед рендерингом объекта User. token = data.get('token', None) if token is not None and isinstance(token, bytes): # Как говорится выше, декодирует token если он имеет тип bytes. data['token'] = token.decode('utf-8') # Наконец, мы можем отобразить наши данные в простанстве имен 'user'. return json.dumps({ 'user': data }) renderer_classes = (UserJSONRenderer,)
from django.contrib.auth import authenticate
class LoginSerializer(serializers.Serializer):
email = serializers.CharField(max_length=255) username = serializers.CharField(max_length=255, read_only=True) password = serializers.CharField(max_length=128, write_only=True) token = serializers.CharField(max_length=255, read_only=True) def validate(self, data): # В методе validate мы убеждаемся, что текущий экземпляр # LoginSerializer значение valid. В случае входа пользователя в систему # это означает подтверждение того, что присутствуют адрес электронной # почты и то, что эта комбинация соответствует одному из пользователей. email = data.get('email', None) password = data.get('password', None) # Вызвать исключение, если не предоставлена почта. if email is None: raise serializers.ValidationError( 'An email address is required to log in.' ) # Вызвать исключение, если не предоставлен пароль. if password is None: raise serializers.ValidationError( 'A password is required to log in.' ) # Метод authenticate предоставляется Django и выполняет проверку, что # предоставленные почта и пароль соответствуют какому-то пользователю в # нашей базе данных. Мы передаем email как username, так как в модели # пользователя USERNAME_FIELD = email. user = authenticate(username=email, password=password) # Если пользователь с данными почтой/паролем не найден, то authenticate # вернет None. Возбудить исключение в таком случае. if user is None: raise serializers.ValidationError( 'A user with this email and password was not found.' ) # Django предоставляет флаг is_active для модели User. Его цель # сообщить, был ли пользователь деактивирован или заблокирован. # Проверить стоит, вызвать исключение в случае True. if not user.is_active: raise serializers.ValidationError( 'This user has been deactivated.' ) # Метод validate должен возвращать словать проверенных данных. Это # данные, которые передются в т.ч. в методы create и update. return { 'email': user.email, 'username': user.username, 'token': user.token } from .serializers import LoginSerializer, RegistrationSerializer
class LoginAPIView(APIView):
permission_classes = (AllowAny,) renderer_classes = (UserJSONRenderer,) serializer_class = LoginSerializer def post(self, request): user = request.data.get('user', {}) # Обратите внимание, что мы не вызываем метод save() сериализатора, как # делали это для регистрации. Дело в том, что в данном случае нам # нечего сохранять. Вместо этого, метод validate() делает все нужное. serializer = self.serializer_class(data=user) serializer.is_valid(raise_exception=True) return Response(serializer.data, status=status.HTTP_200_OK) from .views import LoginAPIView, RegistrationAPIView
urlpatterns = [
path('users/', RegistrationAPIView.as_view()), path('users/login/', LoginAPIView.as_view()), ] {
"user": { "email": "email@email.email", "username": "admin", "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjA1MTE3MjkwfQ.W8B6RY-jGO9PYDTzDWxhrkSHsTe1p3jlzq1BL7Tbwcs" } } from rest_framework.views import exception_handler
def core_exception_handler(exc, context): # Если возникает исключение, которые мы не обрабатываем здесь явно, мы # хотим передать его обработчику исключений по-умолчанию, предлагаемому # DRF. И все же, если мы обрабатываем такой тип исключения, нам нужен # доступ к сгенерированному DRF - получим его заранее здесь. response = exception_handler(exc, context) handlers = { 'ValidationError': _handle_generic_error } # Определить тип текущего исключения. Мы воспользуемся этим сразу далее, # чтобы решить, делать ли это самостоятельно или отдать эту работу DRF. exception_class = exc.__class__.__name__ if exception_class in handlers: # Если это исключение можно обработать - обработать :) В противном # случае, вернуть ответ сгенерированный стандартными средствами заранее return handlers[exception_class](exc, context, response) return response def _handle_generic_error(exc, context, response): # Это самый простой обработчик исключений, который мы можем создать. Мы # берем ответ сгенерированный DRF и заключаем его в ключ 'errors'. response.data = { 'errors': response.data } return response REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'project.exceptions.core_exception_handler', 'NON_FIELD_ERRORS_KEY': 'error', } import json
from rest_framework.renderers import JSONRenderer class UserJSONRenderer(JSONRenderer): charset = 'utf-8' def render(self, data, media_type=None, renderer_context=None): # Если представление выдает ошибку (например, пользователь не может # быть аутентифицирован), data будет содержать ключ error. Мы хотим, # чтобы стандартный JSONRenderer обрабатывал такие ошибки, поэтому # такой случай необходимо проверить. errors = data.get('errors', None) # Если мы получим ключ token как часть ответа, это будет байтовый # объект. Байтовые объекты плохо сериализуются, поэтому нам нужно # декодировать их перед рендерингом объекта User. token = data.get('token', None) if errors is not None: # Позволим стандартному JSONRenderer обрабатывать ошибку. return super(UserJSONRenderer, self).render(data) if token is not None and isinstance(token, bytes): # Как говорится выше, декодирует token если он имеет тип bytes. data['token'] = token.decode('utf-8') # Наконец, мы можем отобразить наши данные в простанстве имен 'user'. return json.dumps({ 'user': data }) class UserSerializer(serializers.ModelSerializer):
""" Ощуществляет сериализацию и десериализацию объектов User. """ # Пароль должен содержать от 8 до 128 символов. Это стандартное правило. Мы # могли бы переопределить это по-своему, но это создаст лишнюю работу для # нас, не добавляя реальных преимуществ, потому оставим все как есть. password = serializers.CharField( max_length=128, min_length=8, write_only=True ) class Meta: model = User fields = ('email', 'username', 'password', 'token',) # Параметр read_only_fields является альтернативой явному указанию поля # с помощью read_only = True, как мы это делали для пароля выше. # Причина, по которой мы хотим использовать здесь 'read_only_fields' # состоит в том, что нам не нужно ничего указывать о поле. В поле # пароля требуются свойства min_length и max_length, # но это не относится к полю токена. read_only_fields = ('token',) def update(self, instance, validated_data): """ Выполняет обновление User. """ # В отличие от других полей, пароли не следует обрабатывать с помощью # setattr. Django предоставляет функцию, которая обрабатывает пароли # хешированием и 'солением'. Это означает, что нам нужно удалить поле # пароля из словаря 'validated_data' перед его использованием далее. password = validated_data.pop('password', None) for key, value in validated_data.items(): # Для ключей, оставшихся в validated_data мы устанавливаем значения # в текущий экземпляр User по одному. setattr(instance, key, value) if password is not None: # 'set_password()' решает все вопросы, связанные с безопасностью # при обновлении пароля, потому нам не нужно беспокоиться об этом. instance.set_password(password) # После того, как все было обновлено, мы должны сохранить наш экземпляр # User. Стоит отметить, что set_password() не сохраняет модель. instance.save() return instance from rest_framework import status
from rest_framework.generics import RetrieveUpdateAPIView from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView from .renderers import UserJSONRenderer from .serializers import ( LoginSerializer, RegistrationSerializer, UserSerializer, ) class UserRetrieveUpdateAPIView(RetrieveUpdateAPIView):
permission_classes = (IsAuthenticated,) renderer_classes = (UserJSONRenderer,) serializer_class = UserSerializer def retrieve(self, request, *args, **kwargs): # Здесь нечего валидировать или сохранять. Мы просто хотим, чтобы # сериализатор обрабатывал преобразования объекта User во что-то, что # можно привести к json и вернуть клиенту. serializer = self.serializer_class(request.user) return Response(serializer.data, status=status.HTTP_200_OK) def update(self, request, *args, **kwargs): serializer_data = request.data.get('user', {}) # Паттерн сериализации, валидирования и сохранения - то, о чем говорили serializer = self.serializer_class( request.user, data=serializer_data, partial=True ) serializer.is_valid(raise_exception=True) serializer.save() return Response(serializer.data, status=status.HTTP_200_OK) from .views import (
LoginAPIView, RegistrationAPIView, UserRetrieveUpdateAPIView ) urlpatterns = [
path('user', UserRetrieveUpdateAPIView.as_view()), path('users/', RegistrationAPIView.as_view()), path('users/login/', LoginAPIView.as_view()), ] {
"user": { "detail": "Authentication credentials were not provided." } } import jwt
from django.conf import settings from rest_framework import authentication, exceptions from .models import User class JWTAuthentication(authentication.BaseAuthentication): authentication_header_prefix = 'Token' def authenticate(self, request): """ Метод authenticate вызывается каждый раз, независимо от того, требует ли того эндпоинт аутентификации. 'authenticate' имеет два возможных возвращаемых значения: 1) None - мы возвращаем None если не хотим аутентифицироваться. Обычно это означает, что мы значем, что аутентификация не удастся. Примером этого является, например, случай, когда токен не включен в заголовок. 2) (user, token) - мы возвращаем комбинацию пользователь/токен тогда, когда аутентификация пройдена успешно. Если ни один из случаев не соблюден, это означает, что произошла ошибка, и мы ничего не возвращаем. В таком случае мы просто вызовем исключение AuthenticationFailed и позволим DRF сделать все остальное. """ request.user = None # 'auth_header' должен быть массивом с двумя элементами: # 1) именем заголовка аутентификации (Token в нашем случае) # 2) сам JWT, по которому мы должны пройти аутентифкацию auth_header = authentication.get_authorization_header(request).split() auth_header_prefix = self.authentication_header_prefix.lower() if not auth_header: return None if len(auth_header) == 1: # Некорректный заголовок токена, в заголовке передан один элемент return None elif len(auth_header) > 2: # Некорректный заголовок токена, какие-то лишние пробельные символы return None # JWT библиотека которую мы используем, обычно некорректно обрабатывает # тип bytes, который обычно используется стандартными библиотеками # Python3 (HINT: использовать PyJWT). Чтобы точно решить это, нам нужно # декодировать prefix и token. Это не самый чистый код, но это хорошее # решение, потому что возможна ошибка, не сделай мы этого. prefix = auth_header[0].decode('utf-8') token = auth_header[1].decode('utf-8') if prefix.lower() != auth_header_prefix: # Префикс заголовка не тот, который мы ожидали - отказ. return None # К настоящему моменту есть "шанс", что аутентификация пройдет успешно. # Мы делегируем фактическую аутентификацию учетных данных методу ниже. return self._authenticate_credentials(request, token) def _authenticate_credentials(self, request, token): """ Попытка аутентификации с предоставленными данными. Если успешно - вернуть пользователя и токен, иначе - сгенерировать исключение. """ try: payload = jwt.decode(token, settings.SECRET_KEY) except Exception: msg = 'Ошибка аутентификации. Невозможно декодировать токеню' raise exceptions.AuthenticationFailed(msg) try: user = User.objects.get(pk=payload['id']) except User.DoesNotExist: msg = 'Пользователь соответствующий данному токену не найден.' raise exceptions.AuthenticationFailed(msg) if not user.is_active: msg = 'Данный пользователь деактивирован.' raise exceptions.AuthenticationFailed(msg) return (user, token) REST_FRAMEWORK = {
... 'DEFAULT_AUTHENTICATION_CLASSES': ( 'apps.authentication.backends.JWTAuthentication', ), } =========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 25-Ноя 11:45
Часовой пояс: UTC + 5