Evgenii Legotckoi
12 вересня 2023 р. 02:47

Django - захищений медіаконтент

На одному з ресурсів, розробкою яких я займаюся, виникла необхідність додавання захищеного доступу до медіа-контенту, з перевіркою права користувача на доступ до цього медіа-контенту. Простіше кажучи, чи може користувач подивитися фотографію, яку віддає nginx як статичний контент.

Насправді, у випадку nginx і django все набагато простіше, ніж здається на перший погляд.

Давайте розберемося з цим на прикладі завантаження та віддачі фотографії.

Алгоритм роботи з фотографією

  • Завантаження фотографії в спеціальну захищену директорію
  • Спроба отримати фотографію по прямому url до фотографії або на сторінці сайту (наприклад тег img)
  • Перевірка прав доступу до фотографії
  • Створення редиректу в nginx у внутрішню захищену директорію на сервері
  • Отримання фотографії

Здається, все просто, а тепер подивимося, що для цього потрібно.

settings.py

Давайте налаштуємо файл конфігурації django сайту. Зазвичай для завантаження медіа контенту та його автоматичної віддачі через nginx у налаштуваннях пишуть щось подібне.

  1. MEDIA_ROOT = BASE_DIR.parent / 'media'
  2. MEDIA_URL = '/media/'

Однак цього разу ми використовуємо спеціальну захищену директорію, яку я завжди називаю protected .

  1. MEDIA_ROOT = BASE_DIR.parent / 'protected'
  2. 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 до захищеної директорії

  1. server {
  2.  
  3. # Other code
  4.  
  5. location /protected/ {
  6. internal;
  7. root /home/www/django_project_root_folder/;
  8. expires 30d;
  9. }
  10. }

Цим кодом ми просто вказуємо, де знаходиться каталог protected і що він є внутрішнім, тобто просто так, без спеціального допуску, користувач не отримає його вміст.

Завантаження фотографій

Для роботи з фотографіями я використовую на даний таку модель даних, звичайно з доопрацюваннями під конкретний проект, але основа одна і та ж

  1. # -*- coding: utf-8 -*-
  2.  
  3. import os
  4. import uuid
  5.  
  6. from django.contrib.gis.db import models
  7. from django.db.models.signals import post_delete
  8. from django.dispatch import receiver
  9. from django.utils.translation import gettext_lazy as _
  10.  
  11. from photo.fields import WEBPField
  12. from photo.managers import PhotoManager
  13.  
  14.  
  15. def image_folder(instance, filename):
  16. return 'photos/{}.webp'.format(uuid.uuid4().hex)
  17.  
  18.  
  19. class Photo(models.Model):
  20. class Meta:
  21. verbose_name = _('Photo')
  22. verbose_name_plural = _('Photos')
  23.  
  24. created_at = models.DateTimeField(verbose_name=_('Created at'), auto_now_add=True)
  25. updated_at = models.DateTimeField(verbose_name=_('Updated at'), auto_now=True)
  26. height = models.IntegerField(verbose_name=_('Height'), default=0, blank=True, null=True)
  27. width = models.IntegerField(verbose_name=_('Width'), default=0, blank=True, null=True)
  28. image = WEBPField(
  29. verbose_name=_('Image'),
  30. upload_to=image_folder,
  31. height_field='height',
  32. width_field='width',
  33. )
  34.  
  35. def filename(self):
  36. return os.path.basename(self.image.name)
  37.  
  38.  
  39. @receiver(post_delete, sender=Photo)
  40. def auto_delete_image_on_delete(sender, instance, **kwargs):
  41. if instance.image:
  42. if os.path.isfile(instance.image.path):
  43. os.remove(instance.image.path)

Тут використовується спеціальне поле WEBPField , про яке я вже розповідав. Це поле, яке літом конвертує зображення у формат webp. Також тут є код для автоматичного видалення фотографій із сервера. Але не це найголовніше.

Найголовнішим є ця функція

  1. def image_folder(instance, filename):
  2. return 'photos/{}.webp'.format(uuid.uuid4().hex)

Ця функція генерує ім'я файлу і вказується куди зберегти файл на сервері щодо protected каталогу.

У результаті фотографія буде збережена наступним шляхом

/home/www/django_project_root_folder/protected/photos/0aec484a6ff246d7ad5eb1b06c0a698e.webp

При цьому в шаблоні код тега img виглядатиме таким чином

  1. <img src="{{ photo.image.url }}"/>

А результат виглядатиме так

  1. <img src="/media/photos/0aec484a6ff246d7ad5eb1b06c0a698e.webp"/>

urls.py

Я почну з вказівки диспетчера URL для отримання доступу до контенту

  1. # -*- coding: utf-8 -*-
  2.  
  3. from django.urls import path
  4.  
  5. from photo.views import photo_access
  6.  
  7. urlpatterns = [
  8. path('media/photos/<str:path>', photo_access, name='photo'),
  9. ]

Як бачите, тут вказано url, який відповідає результуючому для тега img.

Функція photo_access

А тепер найцікавіше, як саме здійснюється доступ до захищеного контенту

  1. # -*- coding: utf-8 -*-
  2.  
  3. from django.http import HttpResponse, HttpResponseForbidden
  4.  
  5. from photo.models import Photo
  6. from utils.shortcuts import get_object_or_none
  7.  
  8.  
  9. def photo_access(request, path):
  10. def create_x_accel_redirect(path):
  11. response = HttpResponse()
  12. # Content-type will be detected by nginx
  13. del response['Content-Type']
  14. response['X-Accel-Redirect'] = '/protected/photos/' + path
  15. return response
  16.  
  17. photo = get_object_or_none(Photo, image='photos/' + path)
  18. if photo is None:
  19. return HttpResponseForbidden('Not authorized to access this media.')
  20.  
  21. # Some another check code
  22. if condition is True:
  23. return create_x_accel_redirect(path)
  24.  
  25. if request.user.is_authenticated and request.user.is_staff:
  26. return create_x_accel_redirect(path)
  27.  
  28. 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

По статті запитували0питання

1

Вам це подобається? Поділіться в соціальних мережах!

Коментарі

Only authorized users can post comments.
Please, Log in or Sign up
  • Останні коментарі
  • Evgenii Legotckoi
    16 квітня 2025 р. 17:08
    Благодарю за отзыв. И вам желаю всяческих успехов!
  • IscanderChe
    12 квітня 2025 р. 17:12
    Добрый день. Спасибо Вам за этот проект и отдельно за ответы на форуме, которые мне очень помогли в некоммерческих пет-проектах. Профессиональным программистом я так и не стал, но узнал мно…
  • AK
    01 квітня 2025 р. 11:41
    Добрый день. В данный момент работаю над проектом, где необходимо выводить звук из программы в определенное аудиоустройство (колонки, наушники, виртуальный кабель и т.д). Пишу на Qt5.12.12 поско…
  • Evgenii Legotckoi
    09 березня 2025 р. 21:02
    К сожалению, я этого подсказать не могу, поскольку у меня нет необходимости в обходе блокировок и т.д. Поэтому я и не задавался решением этой проблемы. Ну выглядит так, что вам действитель…
  • VP
    09 березня 2025 р. 16:14
    Здравствуйте! Я устанавливал Qt6 из исходников а также Qt Creator по отдельности. Все компоненты, связанные с разработкой для Android, установлены. Кроме одного... Когда пытаюсь скомпилиров…