August 2

Как мы скрываем файлы от публичного доступа

Сегодня мы раскроем секретные ингредиенты, которые делают наши технологии уникальными. Наш шеф-СТО Евгений Хацко расскажет о том, как приготовить Django Nginx Secure Link. Приятного прочтения.

Назначение модуля

Мы сделали первый релиз нашего пакета, его можно встретить вот здесь. Мотивацией послужило наличие проблемы со скрытием файлов от публичного доступа для неавторизованных пользователей или, если идти глубже, и вовсе скрыть файл ото всех, создавая временные ссылки под каждый отдельный файл.

Возможно, что кто-то увидит в этом отклики объектного хранилища и скажет «есть же AWS S3, Azure Blobs, etc», но здесь существуют некоторые проблемы и сложности, которые препятствуют быстрой интеграции такого рода хранилищ. Не всегда видится целесообразным подключать в работающий проект объектное хранилище, вот несколько таких примеров из нашего опыта:

  • Проекты, которым не требуется масштабирование зачастую могут быть реализованы простейшим набором инструментов и технологий, не требуя подключения доп сервисов, распределенных по разным узлам (объектное хранилище). Заведомо уменьшая сложность проекта и количество зависимым друг от друга компонентов.
  • Проекты, в которых работа с файлами обусловлена лишь обменом некоторой отчетности, закрытой в рамках одного узла. В таких случаях генерация файлов-отчетов с их последующей выгрузкой в объектное хранилище выглядит избыточной (не всегда, конечно), ведь намного проще следить за файлами в рамках одного узла, закрытого от публичного доступа и уменьшить количество ошибок при их передаче.
  • Простой MVP-проект, в котором важно быстро выпустить продукт с минимальными затратами, без лишних блокеров и зависимостей. Намного проще взять 1 мощный сервер и поднять на нем все необходимые компоненты, такие как СУБД, очереди, веб-сервер приложения и т. д. На практике это самый быстрый и рабочий вариант по скорости настройки и разработки, ну конечно же, учитывая ожидаемые нагрузки и невозможность горизонтального масштабирования.
  • Legacy-проект, в котором интеграция объектного хранилища выглядит трудозатратной и сложной из-за старых версий фреймворка и его зависимостей, а зачастую и вовсе невозможностью его интеграции.
  • Ну и конечно же проекты, для которых объектное хранилище лишь усложнит работу с файлами. В качестве примера можем привести обработку крупного файла в фоновом режиме, которая влечет за собой загрузку файла обратно на сервер из объектного хранилища, чтобы распаковать его или же провести любую другую обработку.

Это лишь малый список примеров, в которых намного проще взять Nginx-модуль, закрыв публичный доступ к файлам, добавив компонент генерации приватных ссылок для выдачи файлов наружу, тем самым не меняя всю логику работы кодовой базы с файлами. Наш пакет позволяет на простых и не только проектах, где Nginx выступает frontend-сервером делегировать защиту файлов непосредственно на веб-сервер, разгружая наш backend.

Решение подходит и для достаточно сложных по архитектуре решений, где файлы могут лежать на отдельных серверах, доступ к которым будет осуществлен через Nginx.

Наш пакет позволяет брать на себя генерацию временных ссылок на файлы, ограничивая публичный доступ, а сам Nginx уже проверяет эти ссылки и принимает решение об отдаче файла. У Nginx есть отличный модуль ngx_http_secure_link_module для такой задачи, мы же взяли его за основу и написали прослойку между Django и данным модулем. Решение получилось простым и удобным, мы также учли специфичные случаи, когда требуется скрыть только конкретные директории из хранилища media или же от обратного — сделать только их публичными, все это конфигурируется на уровне пакета и должно быть настроено на уровне Nginx с указанием конкретных locations для media/...

Пакет также содержит вспомогательные manage.py команды, нацеленные на помощь в конфигурации нужного блока для location.

Работа еще кипит и будут вноситься улучшения и правки. Есть несколько открытых задач, которые в скором времени будут включены в очередной релиз. Если есть интерес к данному пакеты и желание его улучшить , то пишите и участвуйте. Будем рады любой вовлеченности, даже похейтить можно с целью улучшения пакеты.

Давайте немного погрузимся в техническую часть и рассмотрим пример настройки Django-проекта и конфиграции для виртуального хоста Nginx.

Шаг 1: Установка модуля

pip install django-nginx-secure-links

Шаг 2: Настройка Django-проекта

В зависимости от версии Django необходимо указать следующие настройки:

Общие настройки для всех версий Django

INSTALLED_APPS = (
    ...,

    'nginx_secure_links',
)
SECURE_LINK_EXPIRATION_SECONDS = 100
SECURE_LINK_SECRET_KEY = '8SypVsPwf3PypUfdVmos9NdmQNCsMG'
SECURE_LINK_TOKEN_FIELD = 'token'
SECURE_LINK_EXPIRES_FIELD = 'exp'
SECURE_LINK_PRIVATE_PREFIXES = [
    'private',
]
SECURE_LINK_PUBLIC_PREFIXES = []
  • Для версий Django < 4.2:
DEFAULT_FILE_STORAGE = 'nginx_secure_links.storages.FileStorage'
  • Для версий Django >= 4.2:
STORAGES = {
    "default": {
        "BACKEND": "nginx_secure_links.storages.FileStorage",
    },
    "staticfiles": {
        "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
    },
}

Также в качестве примера укажем настройку MEDIA_URL и MEDIA_ROOT

MEDIA_URL = '/media/'
MEDIA_ROOT = '/var/www/html/media/'

Шаг 3: Настройка файла виртуального хоста Nginx

Мы добавили вспомогательную команду secure_links_nginx_location для manage.py, которую удобно использовать для автоматической генерации блока необходимых location, учитывая текущие настройки Django-проекта:

python manage.py secure_links_nginx_location

В консоль будет выведен пример блока конфигурации, который и нужно вставить в файл site.conf:

location /media/private/ {
    secure_link $arg_token,$arg_exp;
    secure_link_md5 "$secure_link_expires$uri 8SypVsPwf3PypUfdVmos9NdmQNCsMG";
    if ($secure_link = "") {
        return 403;
    }
    if ($secure_link = "0") {
        return 410;
    }
    alias /var/www/html/media/private/;
}

location /media/ {
    alias /var/www/html/media/;
}

Шаг 4: Проверка и тестирование

1. Рассмотрим пример модели данных, которая сохраняет файлы в приватную директорию, указанную в настройке SECURE_LINK_PRIVATE_PREFIXES:

from django.db import models


class PrivateDocument(models.Model):
    file = models.FileField(upload_to='private/documents')

2. Создадим новую запись в БД, загрузив файл через административную панель для PrivateDocument.

3. Чтобы сгенерировать защищенную ссылку, достаточно обратиться к свойству url у поля file -> obj.file.url:

from django.http import HttpResponse
from django.shortcuts import get_object_or_404

from models import PrivateDocument


def private_document_view(request, pk):
    obj = get_object_or_404(PrivateDocument, pk=pk)
    private_url = obj.file.url
    return HttpResponse(private_url)

Переменная private_url будет хранить приватную ссылку, содержащую два дополнительных GET-параметра:

  • Пример ссылки: /media/private/documents/file1.txt?token=S9tBKKkXcAq77g8MrVe6LQ&exp=1721519555
  • GET-параметр token (см. настройку SECURE_LINK_TOKEN_FIELD)
  • GET-параметр exp (см. настройку SECURE_LINK_EXPIRES_FIELD)

Стоить учесть, что если файлы не находятся в приватных директория, перечисленных в настройке SECURE_LINK_PRIVATE_PREFIXES, то генерация приватной ссылки будет пропущена. В результате мы получим публичную ссылку без дополнительных GET-параметров token и exp.

Полный рабочий пример под разные версии Django можно найти по этой ссылке. В примере приводится простейшая установка Nginx-модуля через пакет nginx-extras, но стоит отметить, что в случае сборки Nginx из исходного кода можно также добавить данный модуль на шаге ./configure, избегая установки nginx-extras в систему. Мы оформили репозиторий с примером, создав ветки под конкретные версии Django-фреймворка, надеемся это упростит интеграцию пакета в ваши проекты.