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

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

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

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

1

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

Коментарі

Only authorized users can post comments.
Please, Log in or Sign up