Содержание

Файлы пользователей в веб-приложении лучше хранить в объектном хранилище, совместимом с S3, а не на диске сервера: оно не теряет данные при смене серверов, масштабируется без усилий и дешевле дисков. Публичные файлы отдаются через CDN, приватные — по временным подписанным ссылкам, а загрузка проверяется на тип, размер и содержимое, потому что файл от пользователя — один из главных векторов атак.

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

Где хранить файлы пользователей?

Файлы пользователей хранят в объектном хранилище, а в базе данных — только метаданные: путь к файлу, владельца, тип, размер. Диск сервера подходит для одного сервера и прототипа, но ломается при масштабировании: второй сервер не видит файлы первого, а при переезде их легко потерять.

Вариант Плюсы Минусы Когда подходит
Диск сервера Просто, без внешних сервисов Не масштабируется, риск потери при сбое сервера Прототип, один сервер
Объектное хранилище (S3-совместимое) Надёжно, масштабируется, дёшево Нужна настройка доступов Большинство продуктов
База данных Всё в одном месте Раздувает базу, медленные бэкапы Почти никогда для файлов

Большинство облачных провайдеров предлагает хранилища с S3-совместимым API, поэтому код приложения не зависит от конкретного облака.

Как правильно принимать загрузки?

Загрузки принимают с проверкой размера, типа и содержимого, сохраняют под случайным именем и никогда не исполняют. Имя и расширение, которые прислал пользователь, доверия не заслуживают: вредоносный скрипт легко назвать photo.jpg.

public function store(Request $request): JsonResponse
{
    $request->validate([
        // Тип проверяется по содержимому файла, а не по расширению
        'file' => ['required', 'file', 'max:10240', 'mimes:jpg,png,webp,pdf'],
    ]);

    // Случайное имя и приватный каталог; оригинальное имя — только в метаданных
    $path = $request->file('file')->store("uploads/{$request->user()->id}", 's3');

    $upload = $request->user()->uploads()->create([
        'path'          => $path,
        'original_name' => $request->file('file')->getClientOriginalName(),
        'size'          => $request->file('file')->getSize(),
    ]);

    return response()->json(['id' => $upload->id], 201);
}

Правила безопасной загрузки

  1. Ограничение размера и списка разрешённых типов.
  2. Проверка типа по содержимому, а не по расширению.
  3. Случайные имена файлов, без путей от пользователя.
  4. Хранилище отдельно от кода приложения — файлы нельзя исполнить.
  5. Для изображений — перекодирование, которое заодно удаляет метаданные вроде геолокации.
  6. Для документов от незнакомых пользователей — проверка антивирусом.

Уязвимости загрузки файлов входят в общий список рисков веб-приложений — о нём в статье OWASP Top 10 простыми словами.

Экран с программным кодом Фото: Unsplash

Как отдавать публичные и приватные файлы?

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

// Ссылка на приватный документ живёт 10 минут и выдаётся только владельцу
Gate::authorize('view', $upload);

$url = Storage::disk('s3')->temporaryUrl($upload->path, now()->addMinutes(10));

Что учесть:

  • приватные файлы никогда не лежат в публичной папке или публичном бакете;
  • права проверяются при выдаче ссылки — как и для любых других данных;
  • для изображений полезны версии разных размеров, созданные при загрузке или на лету;
  • у публичных файлов имя с хешем содержимого, чтобы кеш CDN обновлялся автоматически — подробнее в статье кеширование в веб-приложении.

Как загружать большие файлы?

Большие файлы — видео, архивы, крупные документы — загружают напрямую в хранилище, минуя сервер приложения: сервер выдаёт браузеру подписанную ссылку на загрузку, а браузер отправляет файл в хранилище частями. Так сервер не держит соединение минутами и не упирается в лимиты памяти.

Порядок загрузки:

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

Сколько стоит хранение и как его сократить?

Хранение в объектных хранилищах стоит недорого, но расходы растут за счёт исходящего трафика, числа запросов и файлов, которые никто не удаляет. Контроль затрат строится на жизненном цикле файлов и CDN.

Что помогает:

  1. Удалять временные и неподтверждённые загрузки автоматически.
  2. Переносить старые редко используемые файлы в более дешёвые классы хранения.
  3. Отдавать публичные файлы через CDN, чтобы снизить исходящий трафик хранилища.
  4. Сжимать изображения и хранить их в современных форматах.

Коротко

  • Храните файлы в S3-совместимом объектном хранилище, а в базе — только метаданные.
  • При загрузке проверяйте размер и тип по содержимому, сохраняйте под случайным именем.
  • Публичные файлы — через CDN, приватные — по временным подписанным ссылкам после проверки прав.
  • Большие файлы загружайте напрямую в хранилище по подписанной ссылке, обработку делайте в фоне.
  • Настройте автоматическое удаление ненужного и перенос старых файлов в дешёвые классы хранения.

Частые вопросы

  • Можно ли хранить файлы в базе данных?

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

  • Нужно ли делать бэкапы объектного хранилища?

    Хранилища надёжно защищают от поломки оборудования, но не от ошибочного удаления. Включите версионирование объектов или копирование в другой регион для важных файлов.

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

    Через подписанные ссылки: сервер после проверки прав выдаёт ссылку, которая работает несколько минут. После истечения срока по ней ничего не скачать, даже если ссылку переслали.