#39. DRF. Authentication, Permissions
REST аутентифікація. Авторизація. Permissions. Фільтрація.
Аутентифікація та її види
Ми з вами розібрали аутентифікацію для роботи класичного веб-додатку, насправді, це був лише один із видів наявних аутентифікацій, давайте розглянемо різні.
Аутентифікація сесією
Заснована на даних сесії, які ми вже розглядали. Авторизація відбувається один раз, після чого інформація про користувача зберігається в “куках” і передається під час кожного запиту.
У чому недоліки такого підходу для REST API?
По-перше, для того щоб виконувати будь-які небезпечні методи POST, PUT, PATCH, DELETE необхідно використовувати CSRF Token, а це означає, що для виконання таких запитів необхідно щоразу робити додатковий запит для
отримання токена.
По-друге, такий підхід передбачає, що сервер зберігає інформацію про сесії, такий підхід не буде RESTful.
Базова аутентифікація
Аутентифікація заснована на тому, що в кожному запиті в хедері запиту буде передаватися логін і пароль необхідного користувача. Найчастіше для використання такого виду аутентифікації в запит додається хедер Authorization із значенням, що складається зі слова Basic і кодованого за допомогою base64 повідомлення виду username:password наприклад:
Authorization: Basic bXl1c2VyOm15cGFzc3dvcmQ= для username - myuser, password - mypassword
Така авторизація вимагає підключення по https, оскільки при звичайному http запит легко буде перехопити і подивитися дані авторизації.
Аутентифікація за токеном
Базова автентифікація має велику кількість плюсів перед сесійною, як мінімум відсутність необхідності робити додаткові запити, але щоразу передавати логін і пароль - це не найзручніший з точки зору безпеки спосіб передачі даних.
Тому найчастішим видом авторизації є авторизація за токеном. Що це означає?
Токен - це спеціальний обчислюваний набір символів, унікальний для кожного користувача.
Наприклад
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9cJSMeKKF2QT4fwpMeJf36POk6
Токен може бути як постійним (практично ніколи не використовується), так і тимчасовим, може перегенеруватися за часу, так і за запитом.
Алгоритм генерації самого токена теж може бути практично будь-яким (Найчастіше просто генерація великої випадкової hex (шістнадцяткового) рядка), як і дані, на яких він ґрунтується (у разі випадкового токена вхідних даних немає, але може бути заснований на будь-яких особистих даних, на мітках часу тощо)
Зовнішня аутентифікація
Завдяки механізму токенів, за авторизацію може відповідати взагалі не ваш сервер. Припустимо, якщо взяти класичну авторизацію через соцмережі, то генератором токена є сама соцмережа, ми лише надаємо дані для авторизації
соцмережі. У відповідь отримуємо токен, при цьому ми поняття не маємо, як саме його генерує умовний фейсбук, але завжди можна переконатися в його правильності, звернувшись до API соцмережі.
За таким же принципом сервером авторизації може бути практично будь-який зовнішній сервер, з яким є попередня домовленість. Припустимо, ви працюєте з командою, яка його розробляє, і можете дізнатися, як цим користуватися.
Для відкритих соцмереж зазвичай є документація з використання їхнього API, де детально написано, як користуватися їхніми авторизацією. Також для таких механізмів існує велика кількість уже написаних packages.
JWT (JSON Web Token)
Реалізація та використання в Django REST Framework
Через CSRF токени авторизація через сесію практично не використовується, тому ми не будемо детально її розглядати
BasicAuthentication
Щоб використовувати Basic аутентифікацію, досить додати в налаштування REST_FRAMEWORK, вsettings.py:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.BasicAuthentication',
]
}
Якщо нам необхідно використовувати кілька аутентифікацій, ми можемо вказати їх у списку, наприклад:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
]
}
Цього достатньо, щоб під час будь-якого запиту спочатку перевірявся хедер авторизації, і в разі правильних логіна і пароля користувача додавали в реквест.
Якщо необхідно додати класи авторизація прямо у в’ю, можна вказати їх через атрибут authentication_classes для Class Based View і такий самий декоратор для функціонального в’ю.
from rest_framework.authentication import SessionAuthentication, BasicAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
class ExampleView(APIView):
authentication_classes = [SessionAuthentication, BasicAuthentication]
def get(self, request, format=None):
content = {
'user': unicode(request.user), # `django.contrib.auth.User` instance.
'auth': unicode(request.auth), # None
}
return Response(content)
@api_view(['GET'])
@authentication_classes([SessionAuthentication, BasicAuthentication])
@permission_classes([IsAuthenticated])
def example_view(request, format=None):
content = {
'user': unicode(request.user), # `django.contrib.auth.User` instance.
'auth': unicode(request.auth), # None
}
return Response(content)
Авторизація за токеном
Якщо необхідно використовувати токен-авторизацію, то DRF надає нам такий функціонал “з коробки”, для цього потрібно додати rest_framework.authtoken у INSTALLED_APPS
INSTALLED_APPS = [
...
'rest_framework.authtoken'
]
І вказати необхідну авторизацію в settings.py:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
...
'rest_framework.authentication.TokenAuthentication',
]
}
Після цього обов’язково потрібно провести міграцію цього додатка python manage.py migrate.
Щоб створити токени для вже наявних юзерів, потрібно зробити це вручну (або написати дата міграцію).
from rest_framework.authtoken.models import Token
token = Token.objects.create(user=...)
print(token.key)
Після цього можна використовувати авторизацію токеном:
Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b
Генерація токенів
Найчастіше генерацію токенів “вішають” на сигнали:
from django.conf import settings
from django.db.models.signals import post_save
from django.dispatch import receiver
from rest_framework.authtoken.models import Token
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs):
if created:
Token.objects.create(user=instance)
Отримання токена
Для отримання токена можна використовувати стандартну в’ю, для цього потрібно додати в URLs obtain_auth_token:
from rest_framework.authtoken import views
urlpatterns += [
path('api-token-auth/', views.obtain_auth_token)
]
Якщо необхідно змінити логіку отримання токена, то це можна зробити, успадкувавши від from rest_framework.authtoken.views import ObtainAuthToken:
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.authtoken.models import Token
from rest_framework.response import Response
class CustomAuthToken(ObtainAuthToken):
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)
return Response({
'token': token.key,
'user_id': user.pk,
'email': user.email
})
Не забувши замінити URLs:
urlpatterns += [
path('api-token-auth/', CustomAuthToken.as_view())
]
Кастомна авторизація
Крім свого токена і свого способу його отримання, можна також розписати і свою власну авторизацію, для цього потрібно успадкувати від базової та описати потрібні методи:
from django.contrib.auth.models import User
from rest_framework import authentication
from rest_framework import exceptions
class ExampleAuthentication(authentication.BaseAuthentication):
def authenticate(self, request):
username = request.META.get('HTTP_X_USERNAME')
if not username:
return None
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
raise exceptions.AuthenticationFailed('No such user')
return (user, None)
Manage-команда drf_create_token
python manage.py drf_create_token <username>
Приймає параметр username і генерує токен для такого юзера, якщо необхідно, то можна перегенерувати за допомогою прапора -r
python manage.py drf_create_token -r <username>
Трохи про реальність
На практиці практично завжди необхідно переписати токен під свої завдання, як мінімум обмежити його час для життя і зробити перегенерацію після закінчення часу життя, зробимо це як практику на цьому занятті.
Зовнішні сервіси
Фактично, кожен окремий сервіс має свою логіку, найчастіше у нас будуть спеціальні пакети для використання таких аутентифікацій, а якщо ні, то їх завжди можна написати. :)
Авторизація для тестування через браузер
У REST фреймворк вбудована можливість тестувати API через браузер, використовуючи сесійну авторизацію. Для цього достатньо додати вбудовані URLs і перейти за цією адресою, після цього за вашими API URLs ви переходитимете як уже авторизований користувач:
urlpatterns += [
path('api-auth/', include('rest_framework.urls')),
]
Permissions
Вони ж права доступу.
Задати дозволи можна на рівні проекту і на рівні ресурсу.
Щоб задати на рівні проєкту, у settings.py необхідно додати:
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
]
}
Для опису на рівні об’єктів використовується аргумент permission_classes:
from rest_framework import permissions
class ExampleModelViewSet(ModelViewSet):
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
Існує досить багато заготовлених пермішенів.
AllowAny - можна всім
IsAuthenticated - тільки авторизованим користувачам
IsAdminUser - тільки адміністраторам
IsAuthenticatedOrReadOnly - залогіненим або тільки на читання
Усі вони спочатку успадковуються від rest_framework.permissons.BasePermission
Але якщо нам потрібні кастомні, то ми можемо створити їх, успадкувавши від permissions.BasePermission і переписавши один або обидва методи has_permisson() і has_object_permission()
Наприклад, власнику можна виконувати будь-які дії, а решті тільки читання об’єкта:
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
"""
Custom permission to allow only owners of an object to edit it.
"""
def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request,
# so we'll always allow GET, HEAD or OPTIONS requests.
if request.method in permissions.SAFE_METHODS:
return True
# Write permissions are only allowed to the owner of the snippet.
return obj.owner == request.user
def has_permission(self, request, view):
return True
has_permission() - відповідає за доступ до списків об’єктів
has_object_permission() - відповідає за доступ до конкретного об’єкта
Пермішени можна вказувати через кому, якщо їх кілька:
permission_classes = [permissions.IsAuthenticatedOrReadOnly,
IsOwnerOrReadOnly]
Якщо у вас немає доступів, ви отримаєте ось таку відповідь:
{
"detail": "Authentication credentials were not provided."
}
Кешування
Використовується декоратор method_decorator і методи cache_page , vary_on_cookieі vary_on_headers.
Використання заголовків Vary
Заголовок Vary визначає, які заголовки запитів механізм кешування повинен враховувати при формуванні ключа кешу. Наприклад, якщо вміст веб-сторінки залежить від мовних уподобань користувача, кажуть, що сторінка “vary on language” (“відрізняється за мовою”).
За замовчуванням, система кешування Django створює ключі кешу, використовуючи запитувану URL-адресу, наприклад, https://www.example.com/stories/2005/?order_by=author.
Це означає, що кожен запит до цієї URL-адреси буде використовувати ту саму кешовану версію, незалежно від відмінностей між користувацькими агентами, таких як файли cookie або мовні уподобання. Однак, якщо ця сторінка видає різний вміст на основі деяких відмінностей у заголовках запитів - таких як файли cookie, мова або user-agent - вам потрібно використовувати заголовок Vary, щоб повідомити механізмам кешування, що вивід сторінки залежить від цих речей.
Щоб зробити це в Django, скористайтеся зручним декоратором вигляду django.views.decorators.vary.vary_on_headers(), наприклад, так:
from django.views.decorators.vary import vary_on_headers
@vary_on_headers("User-Agent")
def my_view(request):
...
У цьому випадку механізм кешування (наприклад, власний проміжний модуль кешування Django) буде кешувати окрему версію сторінки для кожного унікального користувача-агента.
Перевага використання декоратора vary_on_headers замість того, щоб вручну задавати заголовок Vary (використовуючи щось на кшталт response.headers[‘Vary’] = ‘user-agent’) полягає в тому, що декоратор додає до заголовка Vary (який може вже існувати), а не задає його з нуля і потенційно перевизначає все, що там вже було.
Ви можете передати декілька заголовків до vary_on_headers():
@vary_on_headers("User-Agent", "Cookie")
def my_view(request):
...
Це вказує наступним кешам варіювати на обидва, що означає, що кожна комбінація user-agent і cookie отримає своє власне значення кешу. Наприклад, запит з користувацьким агентом Mozilla і значенням cookie foo=bar буде вважатися відмінним від запиту з користувацьким агентом Mozilla і значенням cookie foo=ham.
Оскільки варіювання на основі cookie є дуже поширеним явищем, існує декоратор django.views.decorators.vary.vary_on_cookie(). Ці два представлення еквівалентні:
@vary_on_cookie
def my_view(request):
...
@vary_on_headers("Cookie")
def my_view(request):
...
Заголовки, які ви передаєте в vary_on_headers, не чутливі до регістру; “User-Agent” - це те ж саме, що і “user-agent”.
Використання cache_page
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from django.views.decorators.vary import vary_on_cookie, vary_on_headers
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import viewsets
class UserViewSet(viewsets.ViewSet):
# With cookie: cache requested url for each user for 2 hours
@method_decorator(cache_page(60 * 60 * 2))
@method_decorator(vary_on_cookie)
def list(self, request, format=None):
content = {
"user_feed": request.user.get_user_feed(),
}
return Response(content)
class ProfileView(APIView):
# With auth: cache requested url for each user for 2 hours
@method_decorator(cache_page(60 * 60 * 2))
@method_decorator(vary_on_headers("Authorization"))
def get(self, request, format=None):
content = {
"user_feed": request.user.get_user_feed(),
}
return Response(content)
class PostView(APIView):
# Cache page for the requested url
@method_decorator(cache_page(60 * 60 * 2))
def get(self, request, format=None):
content = {
"title": "Post title",
"body": "Post content",
}
return Response(content)
cache_page декоратор кешує тільки GET і HEAD запити зі статусом 200.
Приклад використання авторизації в ресурсах
class SomeModelViewSet(ModelViewSet):
serializer_class = SomeSerializer
queryset = SomeModel.objects.all()
def perform_create(self, serializer):
serializer.save(user=self.request.user)
Таким чином, ми можемо додавати об’єкт юзера під час збереження нашого серіалайзера.
Фільтрація
DRF надає нам величезні можливості для фільтрації, практично не дописуючи для цього спеціальний код.
SearchFilter
Як і з іншими параметрами, у нас є два варіанти вказівки фільтрації, загальна для всього проєкту або конкретна для певного класу або функції.
Для вказівки загального фільтра на весь проєкт необхідно додати в settings.py у змінну REST_FRAMEWORK:
REST_FRAMEWORK = {
...
'DEFAULT_FILTER_BACKENDS': ['rest_framework.filters.SearchFilter'].
}
Для вказівки в конкретному класі необхідно використовувати аргумент filter_backends. Приймає колекцію з фільтрів, наприклад:
from rest_framework.filters import SearchFilter, OrderingFilter
class GroupViewSet(ModelViewSet):
queryset = Group.objects.all()
filter_backends = [SearchFilter, OrderingFilter]
Або ж відповідний декоратор для використання у функціях.
Як користуватися?
Для використання необхідно додати в клас параметр search_fields
class GroupViewSet(ModelViewSet):
queryset = Group.objects.all()
filter_backends = [SearchFilter, OrderingFilter]
search_fields = ['name', 'label']
Цей параметр також приймає колекцію, що складається зі списку полів, за якими необхідно здійснювати пошук.
Тепер у нас є можливість додати query параметр search= (ключове слово можна поміняти через settings.py, щоб шукати за вказаними полями).
Наприклад:
http://127.0.0.1:9000/api/group/?search=Pyt
Результат буде відфільтровано так, щоб відобразити тільки ті дані, у яких хоча б в одному із зазначених полів буде знайдено частковий збіг без урахування регістру (lookup icontains).
Якщо нам необхідний більш специфічний параметр пошуку, існує 4 спеціальних налаштування в параметрі search_fields:
^Пошук тільки на початку рядка=Повний збіг@Пошук за повним текстом (працює на основі індексів, працює тільки для postgres)$Пошук регулярного виразу
Наприклад:
class GroupViewSet(ModelViewSet):
queryset = Group.objects.all()
filter_backends = [SearchFilter, OrderingFilter]
search_fields = ['=name', '^label']
OrderingFilter
Так само можна додати ordering фільтр для того, щоб вказувати ordering у момент запиту через query параметр ordering= (також можна замінити через settings.py)
Необхідно вказати параметр ordering_fields, також приймає колекцію з полів. Також може приймати спеціальне значення __all__ для можливості сортувати за будь-яким полем.
class GroupViewSet(ModelViewSet):
queryset = Group.objects.all()
filter_backends = [SearchFilter, OrderingFilter]
ordering_fields = ['name', 'label']
У query параметрі може приймати символ - або список полів через кому.
Приклади:
http://example.com/api/users?ordering=username
http://example.com/api/users?ordering=-username
http://example.com/api/users?ordering=account,username
Свій власний фільтр
Як і з усім іншим, можна написати свій власний фільтр, для цього необхідно успадковуватися від rest_framework.filters.BaseFilterBackend і описати один метод filter_queryset, в якому можна описати будь-яку логіку.
Наприклад, цей фільтр відображатиме тільки ті об’єкти, які належать користувачеві.
class IsOwnerFilterBackend(filters.BaseFilterBackend):
"""
Filter that only allows users to see their own objects.
"""
def filter_queryset(self, request, queryset, view):
return queryset.filter(owner=request.user)
Складні комплексні фільтри
Насправді, бувають і значно складніші фільтри, для яких існують спеціальні пакети.
Наприклад:
pip install django-filter
pip install djangorestframework-filters
pip install djangorestframework-word-filter
Усі вони легко налаштовуються і значно розширюють можливість використання фільтрів. Вивчіть їх самостійно.
І на останок ;)
Домашнє завдання:
Додати до модулю наступні можливості
- Basic Authentication.
- Token Authentication.
- Написати viewsets для моделей повернення товару з модуля і створюємо 2 покупки і 1 повернення через postman.
- *Змінити роботу токену так, щоб він був валідний тільки протягом 10 хвилин.
- *Додати фільтр для пошуку тільки своїх покупок, якщо запит від звичайного користувача, і всіх, якщо адміністратор.