Аутентификация по токену. JWT + Djoser

Аутентификация

Аутентификация на веб-сайтах, как правило, устроена так: при входе на сайт вы один раз вводите логин и пароль, система проверяет ваши данные и, в соответствии с вашими правами, предоставляет вам доступ к сайту. При переходе по страницам этого сайта вам не приходится каждый раз заново вводить свои данные: система запоминает вас, «сохраняет ваше состояние».

Но для REST API такой вариант не подходит: отсутствие состояния (stateless) — это один из основных принципов REST. Этот принцип означает, что каждый запрос к серверу не должен быть связан с предыдущими запросами, как будто их и не было.

Если провести аналогию с веб-сайтами — REST предполагает, что при каждом переходе на новую страницу вам снова и снова придётся вводить логин и пароль.

Аутентификация на основе токенов

Аутентификация с помощью токенов отлично вписывается в архитектуру REST. Клиент отправляет токен, кодовую последовательность символов, в заголовке каждого запроса к серверу. Если в базе данных существует пользователь, которому выдавался этот токен, то запрос будет обработан. Если токен не соответствует ни одному пользователю — запрос будет отклонён.

При каждом последующем запросе к API клиент также должен передавать этот токен, и каждый раз будет проводиться проверка прав доступа.

Пишем свою аутентификацию

В актуальной версии Kittygram изменять данные может любой желающий. Это не лучший вариант.

Настроим проект так, чтобы отправлять запросы к сервису могли только аутентифицированные пользователи. Прикрутим к проекту механизм аутентификации по токену.

Порядок действий для пользователя будет таким:

  1. Пользователь создаёт на проекте свою учётную запись, для входа в систему у него будет логин и пароль.
  2. Клиент отправляет запрос на специальный эндпоинт и в запросе передаёт логин и пароль. Если в базе данных существует такой пользователь и пароль совпадает с сохранённым в базе — в ответ клиент получает токен.
  3. Теперь клиент может работать с API, но при каждом запросе он должен отправлять токен.

В DRF есть готовый модуль, который предоставляет возможность аутентификации по токенам прямо «из коробки»: Authtoken.

Настроим этот модуль на примере Kittygram.

Фреймворк DRF в Kittygram уже подключён и настроен (установить DRF самостоятельно можно командой pip install djangorestframework при активированном виртуальном окружении; затем добавить приложение rest_framework в INSTALLED_APPS).

Чтобы подключить механизм получения токена и аутентификации по токену в Django REST framework, нужно выполнить несколько действий:

  1. Подключить модуль authtoken: добавить строку ‘rest_framework.authtoken’ в список INSTALLED_APPS
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'rest_framework.authtoken',
    'cats.apps.CatsConfig',
] 
  1. В settings.py в настройках REST_FRAMEWORK объявить новый способ аутентификации TokenAuthentication.
  2. Запретить доступ всем неаутентифицированным пользователям: ограничение доступа настраивается с помощью пермишенов (англ. permissions, «разрешения»). Чтобы запретить доступ без токена, нужно добавить значение IsAuthenticated для ключа DEFAULT_PERMISSION_CLASSES:
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated', 
    ],

    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.TokenAuthentication',
    ]
} 
  1. Выполнить миграции, чтобы в базе данных создались поля для работы и хранения токена: python manage.py migrate.
  2. Добавить маршрут для получения токена. Подключать можно в любом urls.py, мы подключим в головном:
from rest_framework.authtoken import views


urlpatterns = [

    ...

    path('api-token-auth/', views.obtain_auth_token),
] 

Дополнительную информацию о модуле Authtoken и о его настройках можно найти в документации.

Теперь через Postman можно отправить POST-запрос к адресу api-token-auth/, передать в теле запроса username и password существующего пользователя (например, суперпользователя) — и в ответ придёт заветный токен:

И что с этим делать?

Этот токен надо будет передавать в заголовке каждого запроса, в поле Authorization. Перед самим токеном должно стоять ключевое слово Token.

При последующих запросах к API после успешной аутентификации в объекте запроса будут доступны данные о пользователе:

  • request.user — экземпляр пользователя;
  • request.auth — токен (экземпляр класса rest_framework.authtoken.models.Token).

Этих данных не было в запросе, но Authtoken предоставляет их, основываясь на токене и соответствующей записи в базе данных. Их можно использовать, например, для того, чтобы сопоставить аутентифицированного пользователя с пользователем, который создавал соответствующий объект в БД.

В ответ на запрос без токена клиент получит отказ, статус-код HTTP 401 Unauthorized:

В случае указания недействительного токена (или токена с ошибкой) ответ будет таким:

{
    "detail": "Invalid token."
} 

Аутентификация по JWT-токену

Есть и более продвинутый способ аутентификации — по JWT-токену.

Преимущества JWT-токена в том, что прямо в нём записана информация о пользователе и сроке годности токена; системе не нужно каждый раз обращаться к базе данных, чтобы их сопоставить.Токен, созданный по стандарту JWT (JSON Web Token), состоит из трёх частей. Каждая из них записывается в формате JSON:

  • header (англ. «заголовок») содержит служебную информацию;
  • payload (англ. «полезная нагрузка») хранит основные данные токена;
  • signature (англ. «подпись») — подпись, ключ безопасности для защиты информации.

После подготовки каждая из частей кодируется алгоритмом Base64URL. Получившиеся строки разделяются между собой точками:

Header JWT

Header, как правило, содержит два поля:

  • алгоритм создания подписи — обычно применяется алгоритм HMAC-SHA256 или RSA;
  • тип токена — это строка "JWT".
{
"alg": "HS256",
"typ": "JWT"
} 

Payload JWT

Payload хранит тип токена, timestamp со сроком его действия и информацию для аутентификации:

{
  "token_type": "access",
  "exp":1578171903, 
  "user_id":5
} 

Signature JWT

Подпись гарантирует, что содержимое header и payload в токене не было изменено после создания. Специальный алгоритм генерирует подпись на основе содержимого header и payload. При кодировании этот алгоритм использует секретный ключ, который известен только серверу.

С этого момента в учебном проекте Kittygram вы будете работать именно с JWT-токенами. Для этого придётся отключить аутентификацию через Authtoken (если, конечно, вы её подключали).

Практика: подключите и настройте JWT-аутентификацию

Для работы с JWT в Django установите и подключите две библиотеки Djoser и Simple JWT:

pip install djoser djangorestframework_simplejwt 

Обновите файл settings.py:

STALLED_APPS = (
    'django.contrib.auth',
    ...
    'rest_framework',
    'djoser',
) 

Обратите внимание, приложение Djoser должно быть зарегистрировано после django.contrib.auth и rest_framework.

Добавьте новые настройки в settings.py, они сходны с настройками Authtoken:

  • permission;
  • способ аутентификации по умолчанию;
  • минимально необходимые настройки модуля Simple JWT.
from datetime import timedelta

...

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated', 
    ],

    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
}


SIMPLE_JWT = {
    # Устанавливаем срок жизни токена
   'ACCESS_TOKEN_LIFETIME': timedelta(days=1),
   'AUTH_HEADER_TYPES': ('Bearer',),
} 

Проверьте, нет ли в проекте неприменённых миграций: python manage.py migrate.

Измените файл роутинга urls.py:

urlpatterns = [
    ...
    # Djoser создаст набор необходимых эндпоинтов.
    # базовые, для управления пользователями в Django:
    path('auth/', include('djoser.urls')),
    # JWT-эндпоинты, для управления JWT-токенами:
    path('auth/', include('djoser.urls.jwt')),
] 

Готово. Но прежде чем продолжить — посмотрите на некоторые из эндпоинтов, которые стали доступны в API.

Хорошая новость заключается в том, что djoser сделал за вас большую часть тяжёлой работы: он сгенерировал эндпоинты, управляющие токенами и пользователями через API.

Список всех эндпоинтов, которые создаёт djoser, есть в документации.

Для обработки добавленных эндпоинтов djoser использует собственные вьюсеты и сериализаторы.

Если нужно изменить набор полей сериализатора из djoser, то

  • из djoser.serializers импортируется класс сериализатора, который нужно переопределить (например, UserSerializer или UserCreateSerializer; полный список сериализаторов djoser доступен в документации);
  • описывается новый класс сериализатора (он наследуется от импортированного);
  • в новом сериализаторе переопределяется набор полей, используемых по умолчанию.
from djoser.serializers import UserSerializer
...

class CustomUserSerializer(UserSerializer):
    class Meta:
        model = User
        fields = ('email', 'id', 'username', 'first_name', 'last_name') 

Аналогично можно поступить и с вьюсетом, который использовал сериализатор.

from djoser.views import UserViewSet
...
from .serializers import CustomUserSerializer
...


class CustomUserViewSet(UserViewSet):
    ... 

Применение JWT на практике

Теперь пользователя можно создать через API.

Придумайте новую пару «логин-пароль» и отправьте POST-запрос на http://127.0.0.1:8000/auth/users/, передав их в полях username и password.

Если вы всё сделали правильно, вам вернётся HTTP-ответ со статус-кодом 201 Created:

Теперь можно получить токен: отправьте POST-запрос на эндпоинт /auth/jwt/create/, передав действующий логин и пароль в полях username и password.

API вернёт JWT-токен:

# Пример ответа
{
    "refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTYyMDk0MTQ3NywianRpIjoiODUzYzE5MTg5NzMwNDQwNTk1ZjI3ZTBmOTAzZDcxZDEiLCJ1c2VyX2lkIjoxfQ.0vJBPIUZG4MjeU_Q-mhr5Gqjx7sFlO6AShlfeINK8nA",
    "access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjIwODU1Mzc3LCJqdGkiOiJkY2EwNmRiYTEzNWQ0ZjNiODdiZmQ3YzU2Y2ZjNGE0YiIsInVzZXJfaWQiOjF9.eZfkpeNVfKLzBY7U0h5gMdTwUnGP3LjRn5g8EIvWlVg"
} 

Токен вернётся в поле access, а данные из поля refresh пригодятся для обновления токена.

Если ваш токен утрачен, украден или каким-то иным образом скомпрометирован, вам понадобится отключить его и получить новый. Для этого отправьте POST-запрос на тот же адрес /auth/jwt/create/, а в поле refresh передайте refresh-токен.

Этот токен также надо будет передавать в заголовке каждого запроса, в поле Authorization. Перед самим токеном должно стоять ключевое слово Bearer и пробел.

Слово Bearer (англ. «носитель») здесь заменяет слово Token и означает, что за ним следует сам токен.

В Postman существует возможность передавать токен и по-другому: можно выбрать соответствующий тип авторизации во вкладке Authorization и указать JWT-токен уже там.

Сохраните ссылки на документацию рассмотренных библиотек. Вы с ними обязательно встретитесь в ближайшем будущем.





Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: