На одному з ресурсів, розробкою яких я займаюся, виникла необхідність додавання захищеного доступу до медіа-контенту, з перевіркою права користувача на доступ до цього медіа-контенту. Простіше кажучи, чи може користувач подивитися фотографію, яку віддає nginx як статичний контент.
Насправді, у випадку nginx і django все набагато простіше, ніж здається на перший погляд.
Давайте розберемося з цим на прикладі завантаження та віддачі фотографії.
Алгоритм роботи з фотографією
- Завантаження фотографії в спеціальну захищену директорію
- Спроба отримати фотографію по прямому url до фотографії або на сторінці сайту (наприклад тег img)
- Перевірка прав доступу до фотографії
- Створення редиректу в nginx у внутрішню захищену директорію на сервері
- Отримання фотографії
Здається, все просто, а тепер подивимося, що для цього потрібно.
settings.py
Давайте налаштуємо файл конфігурації django сайту. Зазвичай для завантаження медіа контенту та його автоматичної віддачі через nginx у налаштуваннях пишуть щось подібне.
MEDIA_ROOT = BASE_DIR.parent / 'media' MEDIA_URL = '/media/'
Однак цього разу ми використовуємо спеціальну захищену директорію, яку я завжди називаю protected .
MEDIA_ROOT = BASE_DIR.parent / 'protected' MEDIA_URL = '/media/'
Таким чином, ми кажемо, що у зовнішньому просторі це у нас "media" , а в темних чертогах сервера це "protected" .
Такий код може відповідати наступній структурі директорій.
- /home/www/django_project_root_folder - це основна директорія, де знаходиться проект, статичні та медні каталоги, а також віртуальне оточення
- /home/www/django_project_root_folder/protected - захищений медіакаталог
- /home/www/django_project_root_folder/static - директорія зі статичним контентом
- /home/www/django_project_root_folder/django_project - ваша програма django
- /home/www/django_project_root_folder/python_venv - віртуальне оточення
Мені зараз подобається саме така структура, оскільки у разі зміни версії python, тобто оновлення віртуального оточення, можна просто ініціалізувати нове оточення і поміняти пару рядків у конфігурації, щоб додаток сайту запускався вже з нового віртаульного оточення. При цьому не постраждає ні репозиторій проекту, ні статичні файли, ні медіа-контент.
Конфігурація nginx
Наведу маленький шмат конфігурації, яка відповідає безпосередньо за налаштування доступу nginx до захищеної директорії
server { # Other code location /protected/ { internal; root /home/www/django_project_root_folder/; expires 30d; } }
Цим кодом ми просто вказуємо, де знаходиться каталог protected і що він є внутрішнім, тобто просто так, без спеціального допуску, користувач не отримає його вміст.
Завантаження фотографій
Для роботи з фотографіями я використовую на даний таку модель даних, звичайно з доопрацюваннями під конкретний проект, але основа одна і та ж
# -*- coding: utf-8 -*- import os import uuid from django.contrib.gis.db import models from django.db.models.signals import post_delete from django.dispatch import receiver from django.utils.translation import gettext_lazy as _ from photo.fields import WEBPField from photo.managers import PhotoManager def image_folder(instance, filename): return 'photos/{}.webp'.format(uuid.uuid4().hex) class Photo(models.Model): class Meta: verbose_name = _('Photo') verbose_name_plural = _('Photos') created_at = models.DateTimeField(verbose_name=_('Created at'), auto_now_add=True) updated_at = models.DateTimeField(verbose_name=_('Updated at'), auto_now=True) height = models.IntegerField(verbose_name=_('Height'), default=0, blank=True, null=True) width = models.IntegerField(verbose_name=_('Width'), default=0, blank=True, null=True) image = WEBPField( verbose_name=_('Image'), upload_to=image_folder, height_field='height', width_field='width', ) def filename(self): return os.path.basename(self.image.name) @receiver(post_delete, sender=Photo) def auto_delete_image_on_delete(sender, instance, **kwargs): if instance.image: if os.path.isfile(instance.image.path): os.remove(instance.image.path)
Тут використовується спеціальне поле WEBPField , про яке я вже розповідав. Це поле, яке літом конвертує зображення у формат webp. Також тут є код для автоматичного видалення фотографій із сервера. Але не це найголовніше.
Найголовнішим є ця функція
def image_folder(instance, filename): return 'photos/{}.webp'.format(uuid.uuid4().hex)
Ця функція генерує ім'я файлу і вказується куди зберегти файл на сервері щодо protected каталогу.
У результаті фотографія буде збережена наступним шляхом
/home/www/django_project_root_folder/protected/photos/0aec484a6ff246d7ad5eb1b06c0a698e.webp
При цьому в шаблоні код тега img виглядатиме таким чином
<img src="{{ photo.image.url }}"/>
А результат виглядатиме так
<img src="/media/photos/0aec484a6ff246d7ad5eb1b06c0a698e.webp"/>
urls.py
Я почну з вказівки диспетчера URL для отримання доступу до контенту
# -*- coding: utf-8 -*- from django.urls import path from photo.views import photo_access urlpatterns = [ path('media/photos/<str:path>', photo_access, name='photo'), ]
Як бачите, тут вказано url, який відповідає результуючому для тега img.
Функція photo_access
А тепер найцікавіше, як саме здійснюється доступ до захищеного контенту
# -*- coding: utf-8 -*- from django.http import HttpResponse, HttpResponseForbidden from photo.models import Photo from utils.shortcuts import get_object_or_none def photo_access(request, path): def create_x_accel_redirect(path): response = HttpResponse() # Content-type will be detected by nginx del response['Content-Type'] response['X-Accel-Redirect'] = '/protected/photos/' + path return response photo = get_object_or_none(Photo, image='photos/' + path) if photo is None: return HttpResponseForbidden('Not authorized to access this media.') # Some another check code if condition is True: return create_x_accel_redirect(path) if request.user.is_authenticated and request.user.is_staff: return create_x_accel_redirect(path) return HttpResponseForbidden('Not authorized to access this media.')
У цій функції ми перевіряємо, чи користувач авторизований представник адміністрації ресурсу і чи існує об'єкт фотографії. А також можна додати будь-які інші умови.
Якщо вони виконуються, то за допомогою функції create_x_accel_redirect ми додаємо директиву, яка робить редирект запиту всередині nginx на зображення.
Тобто ми замінюємо
/media/photos/0aec484a6ff246d7ad5eb1b06c0a698e.webp
на
/protected/photos/0aec484a6ff246d7ad5eb1b06c0a698e.webp
та перенаправляємо**.
Таким чином, nginx віддає контент із захищеної директорії по media url.
Докладніше з директивою X-Accel-Redirect ви можете ознайомитись на офіційній wiki nginx