Evgenii Legotckoi
Evgenii Legotckoi14 февраля 2022 г. 4:24

Django Rest Framework - Урок 001. Добавление аутентификации по токену

На данный момент я активно работаю над приложение, которое будет работат с REST API сайта на Django. И одним из первых шагов была настройка аутентификации пользователя по токену, но для того, чтобы это заработало, нужно сначала получить токен авторизации.

Давайте рассмотрим, как это можно сделать.


Установка Django Rest Framework

Поскольку это самая первая статья по применению DRF, то опишу более подробный процесс установки и настройки DRF. В Дальнейшем я уже не буду приводить эти инструкции.

Устанавливаем DRF и остальный необходимые пакеты

pip install djangorestframework
pip install markdown       # Markdown support for the browsable API.
pip install django-filter

Далее добавляем приложение 'rest_frameword' в INSTALLED_APPS в файле конфигурации сайта settings.py

INSTALLED_APPS = [
    ...
    'rest_framework',
]

Далее официальная документация предлагает подключить в urls.py файле login и logout View из rest_framework, чтобы можно было удобне пользоваться API в браузере.

Если честно, то я не использую это API, поскольку мне удобнее пользоваться документацией, сгенерированной через swagger, о чём я тоже скажу далее, но я всё равно подключаю данные View, чтобы потом не решать случайные проблемы.

urlpatterns = [
    ...
    path('api-auth/', include('rest_framework.urls')),
]

Далее настраиваем классы аутентификации и прав доступа по умолчанию (также в settingsp.py)

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.BasicAuthentication',
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.TokenAuthentication',
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ],
}

Теперь всё готово для того, чтобы написать точку доступа для получения токена. Но предвартельно я хочу подключить ещё и генерирование swagger документации. Просто так удобнее работать, мне например очень нравится документация redoc, в которой довольно красиво и удобно всё отображается.

Установка Swagger

А теперь сразу подключим swagger на сайте. Я понимаю, что это не относится к теме статьи, но в коде я делаю специальные манипуляции, чтобы подключить специальное отображение документации и считаю, что в дальнейшем это может Вам пригодится, покоскольку:

  • Вы будете в дальнейшем использовать swagger
  • Вам пондобится иногда кастомизировать вывод информации в описании API, а именно для токена у меня была самое специфичное изменение для swagger.

Установим drf-yasg

Для этого руководствуясь документацией выполним следующую команду

pip install -U drf-yasg

После чего модифицируем файл settings.py

INSTALLED_APPS = [
   ...
   'django.contrib.staticfiles',  # required for serving swagger ui's css/js files
   'drf_yasg',
   ...
]

А теперь зарегистрируем swagger в urls сайта

from django.urls import re_path
from drf_yasg import openapi
from drf_yasg.views import get_schema_view
from rest_framework import permissions

schema_view = get_schema_view(
   openapi.Info(
      title="EXAMPLE API",
      default_version='v1',
      description="REST API Documentation",
      contact=openapi.Contact(email="example@example.com"),
   ),
   public=True,
   permission_classes=(permissions.IsAdminUser,),
)

urlpatterns = [
    ...
    re_path(r'^swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
    re_path(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
    re_path(r'^redoc/$', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
]

Если вы внимательно посмотрите на код, то увидите, что я зарегистрировал swagger только для администрации сайта. Если хотите, чтобы документация была открытой, то удалите эту строку

permission_classes=(permissions.IsAdminUser,),

Дополнительно можно добавить информацию в вывод документации о том, какие типы аутентификации используются для API. Это выполняется в файле settings.py.

SWAGGER_SETTINGS = {
   'SECURITY_DEFINITIONS': {
      'Basic': {
            'type': 'basic'
      },
      'Bearer': {
            'type': 'apiKey',
            'name': 'Authorization',
            'in': 'header'
      }
   }
}

Вот теперь можно приступить к реализации точку доступа для получения токена.

Приложение api

Создадим приложение api, в котором будет добавлена конечная точка для получения токена.

python manage.py startapp api

serializers.py

Далее создадим файл с сериализаторами, в котором будет создан специальный сериализатор, который будет описывать ответ сайта в swagger.

# -*- coding: utf-8 -*-

from rest_framework import serializers


class Response201AuthTokenSerializer(serializers.Serializer):
    token = serializers.CharField(required=True, allow_blank=False)
    user_id = serializers.IntegerField(required=True)

Этот сериализатор будет корректно генерировать информацию об ответе в документации, и я его использую для создания JSON ответа с токеном.

views.py

После чего модифицирем файл views.py, чтобы получить конечную точку.

# -*- coding: utf-8 -*-

from django.contrib.auth import get_user_model
from django.http import JsonResponse
from django.utils.translation import gettext_lazy as _
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from rest_framework import authentication, viewsets
from rest_framework.authtoken.views import ObtainAuthToken

from api.serializers import Response201AuthTokenSerializer


class CustomAuthToken(ObtainAuthToken):
    authentication_classes = [authentication.BasicAuthentication]

    @swagger_auto_schema(responses={
        "201": openapi.Response(
            description=_("User has got Token"),
            schema=Response201AuthTokenSerializer,
        )
    })
    def post(self, request, *args, **kwargs):
        serializer = self.serializer_class(data=request.data,
                                           context={'request': request})
        serializer.is_valid(raise_exception=True)
        user = serializer.validated_data['user']
        token, created = Token.objects.get_or_create(user=user)
        response201 = Response201AuthTokenSerializer(data={"token": token.key, "user_id": user.pk})
        response201.is_valid(raise_exception=True)
        return JsonResponse(response201.data)

Как видите в коде имеется кастомизированная схема swagger для ответа 201, контент создан. Из сериализатора Response201AuthTokenSerializer будет сформирована корректная документация.
Помимо прочего я использую Response201AuthTokenSerializer, чтобы сфомировать ответ с токеном, а также пользовательским id. В дальнейшем я использую id пользователя для манипуляции с данными в мобильном приложении.
Отличительным нюансом данного view является то, что в качестве аутентификации используется только BasicAuthenttication, то есть пользователь получает доступ к данном view только через username и password. В дальнейшем разработка API ведётся для использования токена.

urls.py

А теперь подключить view для получения токена в файлах urls

# -*- coding: utf-8 -*-

from django.urls import path

from api import views


app_name = 'api'
urlpatterns = [
    path('api-token-auth/', views.CustomAuthToken.as_view()),
]

В данном файле определяем имя приложения, которое используется для формирования маршрутов, то есть api. А также подключаем view токена к маршрутизации.

И в конце подключаем urls.py приложения api в основном файле urls.py всего проекта.

urlpatterns = [
    ...
    path('api/', include('api.urls')),
    ...
]

Теперь, чтобы получить токен, необходимо направить POST запрос по url http://127.0.0.1/api/api-token-auth/ , в том случае, чтобы тестировать функционал на локальном сервере. В случае боевого сервера нужно отправлять запрос по адресу с доменом вашего сайта.

Для аутентификации нужно использовать Basic аутентификацию.

Пример запроса токена в Felgo QML

Вот пример кода для запроса токена в приложении написанном на QML Felgo

function login(username, password, success, error) {
    HttpRequest
    .post("http://127.0.0.1/api/api-token-auth/", {username: username, password: password})
    .timeout(5000)
    .set('Content-Type', 'application/json')
    .then(function(res) { success(res) })
    .catch(function(err) { error(err) });
}
Рекомендуем хостинг TIMEWEB
Рекомендуем хостинг TIMEWEB
Стабильный хостинг, на котором располагается социальная сеть EVILEG. Для проектов на Django рекомендуем VDS хостинг.

Вам это нравится? Поделитесь в социальных сетях!

BL4CK R4BBIT
  • 14 февраля 2022 г. 6:42

Доброго времени.
А если кастомный юзер.
И активация юзера подтверждением по смс или звонку?

Evgenii Legotckoi
  • 14 февраля 2022 г. 6:54
  • (ред.)

Добрый день

Если Вы имеетe ввиду, что модель пользователя наследована от AbstractUser и переопределена для проекта, то да - этот пример рабочий, я сейчас занимаюсь одним проектом, который будет иметь приложение и использую там кастомного пользователя.

А что касается подтверждения по СМС, то я участвовал несколько лет назад в проекте, где было подтверждение по СМС. Код показать не могу, но суть была в том, что там было два REST API View, сначало приложение стучалось на первый url, куда посылало логин и пароль, при получении ответа 200, приложение меняло форму ввода данных и ждало, пока пользователь заполнит код из СМС и подтвердит отправку. В этом случае отправка кода была уже на второй url, который проверял правильность кода, и уже в этом случае отдавал токен.

Естественно первый URL, который обрабатывал логин и пароль, стучался на API стороннего провайдера по отправке СМС, получал отправленный код и сохранял в отдельную таблицу, чтобы потом сравнить код и соответствие пользователя коду.

BL4CK R4BBIT
  • 14 февраля 2022 г. 7:14
  • (ред.)

В моем случае кастом от AbstractBaseUser

class User(AbstractBaseUser):
    phone       = PhoneNumberField(region='RU', unique=True)
    username    = models.CharField(max_length = 150, blank = True, null = True)
    standard    = models.CharField(max_length = 3, blank = True, null = True)
    score       = models.IntegerField(default = 16)
    first_login = models.BooleanField(default=False)
    active      = models.BooleanField(default=False)
    staff       = models.BooleanField(default=False)
    admin       = models.BooleanField(default=False)
    timestamp   = models.DateTimeField(auto_now_add=True)

    USERNAME_FIELD = 'phone'
    REQUIRED_FIELDS = []

    objects = UserManager()

    def __str__(self):
        return self.phone

    def get_full_name(self):
        return self.phone

    def get_short_name(self):
        return self.phone

    def has_perm(self, perm, obj=None):
        return True

    def has_module_perms(self, app_label):

        return True

    @property
    def is_staff(self):
        return self.staff

    @property
    def is_admin(self):
        return self.admin

    @property
    def is_active(self):
        return self.active

Только токен я использую knox(Стандарт мобильной безопастности Samsung), потому и спросил

Дальше понятно:
Регистрация post на страницу урл api/register(можно сразу отправить запрос на звонок, после создания пользователя)
Если удачно то получим 4-x символьный код от провайдера, запищем его. Если неудачно то создаем свой и шлем смс
Cледом post-запрос на верификацию с кодом(при удаче - активируем пользователя, при не удаче 4 попытки на повтор)

Evgenii Legotckoi
  • 14 февраля 2022 г. 7:22

В таком случае без напильника этот код сходу не взлетит, если конкретно knox нужно использовать.
А касательно самого AbstractBaseUser, то должно заработать.

Комментарии

Только авторизованные пользователи могут публиковать комментарии.
Пожалуйста, авторизуйтесь или зарегистрируйтесь
e
  • ehot
  • 1 апреля 2024 г. 0:29

C++ - Тест 003. Условия и циклы

  • Результат:78баллов,
  • Очки рейтинга2
B

C++ - Тест 002. Константы

  • Результат:16баллов,
  • Очки рейтинга-10
B

C++ - Тест 001. Первая программа и типы данных

  • Результат:46баллов,
  • Очки рейтинга-6
Последние комментарии
k
kmssr9 февраля 2024 г. 5:43
Qt Linux - Урок 001. Автозапуск Qt приложения под Linux как сделать автозапуск для флэтпака, который не даёт создавать файлы в ~/.config - вот это вопрос ))
АК
Анатолий Кононенко5 февраля 2024 г. 12:50
Qt WinAPI - Урок 007. Работаем с ICMP Ping в Qt Без строки #include <QRegularExpressionValidator> в заголовочном файле не работает валидатор.
EVA
EVA25 декабря 2023 г. 21:30
Boost - статическая линковка в CMake проекте под Windows Ошибка LNK1104 часто возникает, когда компоновщик не может найти или открыть файл библиотеки. В вашем случае, это файл libboost_locale-vc142-mt-gd-x64-1_74.lib из библиотеки Boost для C+…
J
JonnyJo25 декабря 2023 г. 19:38
Boost - статическая линковка в CMake проекте под Windows Сделал всё по-как у вас, но выдаёт ошибку [build] LINK : fatal error LNK1104: не удается открыть файл "libboost_locale-vc142-mt-gd-x64-1_74.lib" Хоть убей, не могу понять в чём дел…
G
Gvozdik19 декабря 2023 г. 8:01
Qt/C++ - Урок 056. Подключение библиотеки Boost в Qt для компиляторов MinGW и MSVC Для решения твой проблемы добавь в файл .pro строчку "LIBS += -lws2_32" она решит проблему , лично мне помогло.
Сейчас обсуждают на форуме
a
a_vlasov14 апреля 2024 г. 16:41
Мобильное приложение на C++Qt и бэкенд к нему на Django Rest Framework Евгений, добрый день! Такой вопрос. Верно ли следующее утверждение: Любое Android-приложение, написанное на Java/Kotlin чисто теоретически (пусть и с большими трудностями) можно написать и на C+…
Павел Дорофеев
Павел Дорофеев14 апреля 2024 г. 12:35
QTableWidget с 2 заголовками Вот тут есть кастомный QTableView с многорядностью проект поддерживается, обращайтесь
f
fastrex4 апреля 2024 г. 14:47
Вернуть старое поведение QComboBox, не менять индекс при resetModel Добрый день! У нас много проектов в которых используется QComboBox, в версии 5.5.1, когда модель испускает сигнал resetModel, currentIndex не менялся. В версии 5.15 при resetModel происходит try…
P
Pisych27 февраля 2023 г. 15:04
Как получить в массив значения из связанной модели? Спасибо, разобрался:))
AC
Alexandru Codreanu19 января 2024 г. 22:57
QML Обнулить значения SpinBox Доброго времени суток, не могу разобраться с обнулением значение SpinBox находящего в делегате. import QtQuickimport QtQuick.ControlsWindow { width: 640 height: 480 visible: tr…

Следите за нами в социальных сетях