Аутентификация пользователя с помощью Django REST Framework и веб-токенов JSON
За последние пару месяцев я в свободное время создавал веб-приложение. Хотя я хотел бы держать детали моего приложения в секрете (пока), мне хотелось бы поделиться некоторыми трудностями, с которыми я столкнулся, работая над этим приложением; особенно когда дело доходит до управления пользователями и аутентификации с помощью веб-токенов JSON (JWT).
Стек технологий
Прежде чем углубиться в код, я хотел бы дать некоторую справочную информацию о технологии, которую я буду использовать. В качестве основы я выбрал Django и Django Rest Framework для разработки RESTful API, который был бы в основе моего приложения. Для внешнего интерфейсе я решил использовать Reactjs для создания своего SPA, чтобы делать запросы на сервер. Я буду использовать Docker для контейнеризации, CircleCI для построения конвейера CI/CD и AWS в качестве службы инфраструктуры. Наконец, я буду использовать Postman для запуска тестов и мониторинга API на ходу. Это предполагает предполагает некоторое знакомство с Python, Django и Docker.
Требования
Перед началом работы вы должны установить следующие зависимости Python:
Django==2.1 django-rest-framework==0.1.0 djangorestframework==3.8.2 gunicorn==19.9.0 # Our WSGI server psycopg2==2.7.5 # Only if using Postgresql PyJWT==1.6.4 pytz==2018.5
Поскольку я использую Docker, вы можете приступить к работе со следующим Dockerfile:
FROM python:3.6-alpine MAINTAINER Sebastian Ojeda <sebastian@oddjobbox.com> WORKDIR /app COPY requirements.txt . RUN apk add --no-cache --virtual .build-deps \ build-base postgresql-dev jpeg-dev zlib-dev \ && pip install -r requirements.txt \ && find /usr/local \ \( -type d -a -name test -o -name tests \) \ -o \( -type f -a -name '*.pyc' -o -name '*.pyo' \) \ -exec rm -rf '{}' + \ && runDeps="$( \ scanelf --needed --nobanner --recursive /usr/local \ | awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \ | sort -u \ | xargs -r apk info --installed \ | sort -u \ )" \ && apk add --virtual .rundeps $runDeps \ && apk del .build-deps COPY . . CMD ["python3", "manage.py", "runserver", "0:8000"]
Я все еще работаю над уменьшением размера файла этого образа. А пока этого будет достаточно. Двигаемся дальше!
Создание модели User
Модель пользователя Django довольно проста. Мы будем наследовать от классов AbstractBaseUser
и PermissionsMixin
для создания нашей модели.
import jwt
from datetime import datetime
from datetime import timedelta
from django.conf import settings
from django.db import models
from django.core import validators
from django.contrib.auth.models import AbstractBaseUser
from django.contrib.auth.models import PermissionsMixin
class User(AbstractBaseUser, PermissionsMixin):
"""
Определяет наш пользовательский класс User.
Требуется имя пользователя, адрес электронной почты и пароль.
"""
username = models.CharField(db_index=True, max_length=255, unique=True)
email = models.EmailField(
validators=[validators.validate_email],
unique=True,
blank=False
)
is_staff = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
# Свойство `USERNAME_FIELD` сообщает нам, какое поле мы будем использовать для входа.
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ('username',)
# Сообщает Django, что класс UserManager, определенный выше,
# должен управлять объектами этого типа.
objects = UserManager()
def __str__(self):
"""
Возвращает строковое представление этого `User`.
Эта строка используется, когда в консоли выводится `User`.
"""
return self.username
@property
def token(self):
"""
Позволяет нам получить токен пользователя, вызвав `user.token` вместо
`user.generate_jwt_token().
Декоратор `@property` выше делает это возможным.
`token` называется «динамическим свойством ».
"""
return self._generate_jwt_token()
def get_full_name(self):
"""
Этот метод требуется Django для таких вещей,
как обработка электронной почты.
Обычно это имя и фамилия пользователя.
Поскольку мы не храним настоящее имя пользователя,
мы возвращаем его имя пользователя.
"""
return self.username
def get_short_name(self):
"""
Этот метод требуется Django для таких вещей,
как обработка электронной почты.
Как правило, это будет имя пользователя.
Поскольку мы не храним настоящее имя пользователя,
мы возвращаем его имя пользователя.
"""
return self.username
def _generate_jwt_token(self):
"""
Создает веб-токен JSON, в котором хранится идентификатор
этого пользователя и срок его действия
составляет 60 дней в будущем.
"""
dt = datetime.now() + timedelta(days=60)
token = jwt.encode({
'id': self.pk,
'exp': int(dt.strftime('%s'))
}, settings.SECRET_KEY, algorithm='HS256')
return token.decode('utf-8')
Как видите, токен создается динамически с помощью декоратора @property с истечением 60 дней. UserManager
, который будет вызываться при создании нового пользователя, выглядит следующим образом:
from django.contrib.auth.models import BaseUserManager
class UserManager(BaseUserManager):
"""
Django требует, чтобы пользовательские `User`
определяли свой собственный класс Manager.
Унаследовав от BaseUserManager, мы получаем много кода,
используемого Django для создания `User`.
Все, что нам нужно сделать, это переопределить функцию
`create_user`, которую мы будем использовать
для создания объектов `User`.
"""
def _create_user(self, username, email, password=None, **extra_fields):
if not username:
raise ValueError('Указанное имя пользователя должно быть установлено')
if not email:
raise ValueError('Данный адрес электронной почты должен быть установлен')
email = self.normalize_email(email)
user = self.model(username=username, email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_user(self, username, email, password=None, **extra_fields):
"""
Создает и возвращает `User` с адресом электронной почты,
именем пользователя и паролем.
"""
extra_fields.setdefault('is_staff', False)
extra_fields.setdefault('is_superuser', False)
return self._create_user(username, email, password, **extra_fields)
def create_superuser(self, username, email, password, **extra_fields):
"""
Создает и возвращает пользователя с правами
суперпользователя (администратора).
"""
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
if extra_fields.get('is_staff') is not True:
raise ValueError('Суперпользователь должен иметь is_staff=True.')
if extra_fields.get('is_superuser') is not True:
raise ValueError('Суперпользователь должен иметь is_superuser=True.')
return self._create_user(username, email, password, **extra_fields)
Я рекомендую читать код построчно, чтобы убедиться, что вы понимаете, что происходит (это, как правило, хорошая идея, когда вы копируете код из Интернета). Класс User
и UserManager
- все, что вам нужно для создания пользовательского пользователя в Django. Просто не забудьте сообщить Django, что эти модели существуют, объявив ваше приложение в файле settings.py
:
INSTALLED_APPS = [
...
'rest_framework',
'authentication', # My'authentication` app
...
]
AUTH_USER_MODEL = 'authentication.User'
Бэкенд аутентификации
По умолчанию Django не знает, как аутентифицировать ваши JWT. Чтобы это исправить, мы должны создать следующий файл 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 = 'Bearer'
def authenticate(self, request):
"""
The `authenticate` method is called on every request regardless of
whether the endpoint requires authentication.
`authenticate` has two possible return values:
1) `None` - We return `None` if we do not wish to authenticate. Usually
this means we know authentication will fail. An example of
this is when the request does not include a token in the
headers.
2) `(user, token)` - We return a user/token combination when
authentication is successful.
If neither case is met, that means there's an error
and we do not return anything.
We simple raise the `AuthenticationFailed`
exception and let Django REST Framework
handle the rest.
"""
request.user = None
# `auth_header` should be an array with two elements: 1) the name of
# the authentication header (in this case, "Token") and 2) the JWT
# that we should authenticate against.
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:
# Invalid token header. No credentials provided. Do not attempt to
# authenticate.
return None
elif len(auth_header) > 2:
# Invalid token header. The Token string should not contain spaces.
# Do not attempt to authenticate.
return None
# The JWT library we're using can't handle the `byte` type, which is
# commonly used by standard libraries in Python 3. To get around this,
# we simply have to decode `prefix` and `token`. This does not make for
# clean code, but it is a good decision because we would get an error
# if we didn't decode these values.
prefix = auth_header[0].decode('utf-8')
token = auth_header[1].decode('utf-8')
if prefix.lower() != auth_header_prefix:
# The auth header prefix is not what we expected. Do not attempt to
# authenticate.
return None
# By now, we are sure there is a *chance* that authentication will
# succeed. We delegate the actual credentials authentication to the
# method below.
return self._authenticate_credentials(request, token)
def _authenticate_credentials(self, request, token):
"""
Try to authenticate the given credentials. If authentication is
successful, return the user and token. If not, throw an error.
"""
try:
payload = jwt.decode(token, settings.SECRET_KEY)
except:
msg = 'Invalid authentication. Could not decode token.'
raise exceptions.AuthenticationFailed(msg)
try:
user = User.objects.get(pk=payload['id'])
except User.DoesNotExist:
msg = 'No user matching this token was found.'
raise exceptions.AuthenticationFailed(msg)
if not user.is_active:
msg = 'This user has been deactivated.'
raise exceptions.AuthenticationFailed(msg)
return (user, token)
Опять же, много кода пропускается, но мне нравится думать, что это довольно просто, если у вас есть некоторый опыт работы с Python и Django.
Мы также должны не забыть обновить наш файл settings.py
, чтобы сообщить Django, где найти наш пользовательский сервер аутентификации:
...
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'authentication.backends.JWTAuthentication',
)
}
...
К настоящему времени вы создали пользовательскую модель User
и модель UserManager
и создали собственный класс JWTAuthentication
для аутентификации ваших пользовательских токенов. Последним отсутствующим элементом является настройка пользовательских представлений для обработки DRF.
DRF сериализаторы
Есть пара видов, которые нужно сериализовать, чтобы наконец начать работу (узнать больше о сериализаторах Django Rest Framework). Первый - это RegistrationSerializer
from rest_framework import serializers
from .models import User
class RegistrationSerializer(serializers.ModelSerializer):
"""
Creates a new user.
Email, username, and password are required.
Returns a JSON web token.
"""
# The password must be validated and should not be read by the client
password = serializers.CharField(
max_length=128,
min_length=8,
write_only=True,
)
# The client should not be able to send a token along with a registration
# request. Making `token` read-only handles that for us.
token = serializers.CharField(max_length=255, read_only=True)
class Meta:
model = User
fields = ('email', 'username', 'password', 'token',)
def create(self, validated_data):
return User.objects.create_user(**validated_data)
Этот сериализатор получит имя пользователя, адрес электронной почты и пароль и вернет маркер пользователя, если аутентификация прошла успешно. Далее нам нужен способ авторизации существующего пользователя. Для этого мы создадим LoginSerializer
:
class LoginSerializer(serializers.Serializer):
"""
Authenticates an existing user.
Email and password are required.
Returns a JSON web token.
"""
email = serializers.EmailField(write_only=True)
password = serializers.CharField(max_length=128, write_only=True)
# Ignore these fields if they are included in the request.
username = serializers.CharField(max_length=255, read_only=True)
token = serializers.CharField(max_length=255, read_only=True)
def validate(self, data):
"""
Validates user data.
"""
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.'
)
user = authenticate(username=email, password=password)
if user is None:
raise serializers.ValidationError(
'A user with this email and password was not found.'
)
if not user.is_active:
raise serializers.ValidationError(
'This user has been deactivated.'
)
return {
'token': user.token,
}
DRF Views
Процесс входа также вернет токен пользователя, но только если пользователь уже создан. С этими двумя сериализаторами мы можем перейти к нашему файлу view.py
. Нам просто нужно включить представление для регистрации и входа в систему (узнать больше о представлениях DRF).
from rest_framework import status
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView
from .models import User
from .serializers import LoginSerializer
from .serializers import RegistrationSerializer
class RegistrationAPIView(APIView):
"""
Registers a new user.
"""
permission_classes = [AllowAny]
serializer_class = RegistrationSerializer
def post(self, request):
"""
Creates a new User object.
Username, email, and password are required.
Returns a JSON web token.
"""
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(
{
'token': serializer.data.get('token', None),
},
status=status.HTTP_201_CREATED,
)
class LoginAPIView(APIView):
"""
Logs in an existing user.
"""
permission_classes = [AllowAny]
serializer_class = LoginSerializer
def post(self, request):
"""
Checks is user exists.
Email and password are required.
Returns a JSON web token.
"""
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
return Response(serializer.data, status=status.HTTP_200_OK)
Последний шаг - настроить наш файл urls.py для сопоставления наших представлений с URL.
from django.urls import re_path, include
from .views import RegistrationAPIView
from .views import LoginAPIView
urlpatterns = [
re_path(r'^registration/?$', RegistrationAPIView.as_view(), name='user_registration'),
re_path(r'^login/?$', LoginAPIView.as_view(), name='user_login'),
]
Резюме
Создав все эти файлы, мы теперь можем регистрировать и авторизовать пользователей с помощью наших пользовательских моделей Django и успешно аутентифицировать наших пользователей с помощью JSON Web Tokens. Хотя большая часть этой информации была размещена на этой странице, я надеюсь, что она помогла тем, кто хочет сделать что-то подобное.
Перевод https://refactrd.com/user-authentication-with-django-rest-framework-and-json-web-tokens/
Вернуться на верх