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