Содержание
Файлы пользователей в веб-приложении лучше хранить в объектном хранилище, совместимом с 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);
}
Правила безопасной загрузки
- Ограничение размера и списка разрешённых типов.
- Проверка типа по содержимому, а не по расширению.
- Случайные имена файлов, без путей от пользователя.
- Хранилище отдельно от кода приложения — файлы нельзя исполнить.
- Для изображений — перекодирование, которое заодно удаляет метаданные вроде геолокации.
- Для документов от незнакомых пользователей — проверка антивирусом.
Уязвимости загрузки файлов входят в общий список рисков веб-приложений — о нём в статье OWASP Top 10 простыми словами.
Фото: Unsplash
Как отдавать публичные и приватные файлы?
Публичные файлы — изображения каталога, аватары, файлы на страницах — отдают через CDN с долгим кешированием. Приватные — документы, счета, файлы из личного кабинета — отдают по временным подписанным ссылкам, которые проверяют права пользователя и перестают работать через несколько минут.
// Ссылка на приватный документ живёт 10 минут и выдаётся только владельцу
Gate::authorize('view', $upload);
$url = Storage::disk('s3')->temporaryUrl($upload->path, now()->addMinutes(10));
Что учесть:
- приватные файлы никогда не лежат в публичной папке или публичном бакете;
- права проверяются при выдаче ссылки — как и для любых других данных;
- для изображений полезны версии разных размеров, созданные при загрузке или на лету;
- у публичных файлов имя с хешем содержимого, чтобы кеш CDN обновлялся автоматически — подробнее в статье кеширование в веб-приложении.
Как загружать большие файлы?
Большие файлы — видео, архивы, крупные документы — загружают напрямую в хранилище, минуя сервер приложения: сервер выдаёт браузеру подписанную ссылку на загрузку, а браузер отправляет файл в хранилище частями. Так сервер не держит соединение минутами и не упирается в лимиты памяти.
Порядок загрузки:
- клиент запрашивает у сервера разрешение на загрузку с указанием типа и размера;
- сервер проверяет права и лимиты и выдаёт подписанную ссылку;
- клиент загружает файл в хранилище, при необходимости — частями с возобновлением;
- хранилище или клиент сообщает серверу о завершении, сервер проверяет файл и сохраняет метаданные;
- обработка — превью, конвертация, проверка антивирусом — выполняется в фоновой задаче.
Сколько стоит хранение и как его сократить?
Хранение в объектных хранилищах стоит недорого, но расходы растут за счёт исходящего трафика, числа запросов и файлов, которые никто не удаляет. Контроль затрат строится на жизненном цикле файлов и CDN.
Что помогает:
- Удалять временные и неподтверждённые загрузки автоматически.
- Переносить старые редко используемые файлы в более дешёвые классы хранения.
- Отдавать публичные файлы через CDN, чтобы снизить исходящий трафик хранилища.
- Сжимать изображения и хранить их в современных форматах.
Коротко
- Храните файлы в S3-совместимом объектном хранилище, а в базе — только метаданные.
- При загрузке проверяйте размер и тип по содержимому, сохраняйте под случайным именем.
- Публичные файлы — через CDN, приватные — по временным подписанным ссылкам после проверки прав.
- Большие файлы загружайте напрямую в хранилище по подписанной ссылке, обработку делайте в фоне.
- Настройте автоматическое удаление ненужного и перенос старых файлов в дешёвые классы хранения.
Частые вопросы
Можно ли хранить файлы в базе данных?
Технически можно, но это раздувает базу, замедляет бэкапы и репликацию. Практически всегда лучше хранить файлы в объектном хранилище, а в базе держать ссылку и метаданные.
Нужно ли делать бэкапы объектного хранилища?
Хранилища надёжно защищают от поломки оборудования, но не от ошибочного удаления. Включите версионирование объектов или копирование в другой регион для важных файлов.
Как ограничить доступ к файлам по времени?
Через подписанные ссылки: сервер после проверки прав выдаёт ссылку, которая работает несколько минут. После истечения срока по ней ничего не скачать, даже если ссылку переслали.



