Перейти к содержанию
Innovialabs

Загрузка файлов в объектное хранилище через presigned URL

Продвинутое3 мин

Коротко

Сервер выдает браузеру короткоживущую подписанную ссылку, и файл уходит прямо в хранилище, минуя бэкенд; ограничения размера и типа задаются в подписи (лучше через presigned POST), а CORS разрешает загрузку только с вашего домена.

Суть подхода

Классическая схема «браузер → ваш сервер → хранилище» заставляет бэкенд принимать каждый байт, держать соединение открытым и платить за двойной трафик. Presigned URL — это ссылка с криптографической подписью, которую сервер генерирует своими ключами доступа. Браузер получает ее и загружает файл напрямую в S3-совместимое хранилище (AWS S3, Cloudflare R2, Yandex Object Storage, MinIO и другие).

Ключи при этом никогда не попадают на клиент: подпись разрешает ровно одно действие с одним объектом и только до истечения срока.

Почему это лучше проксирования

Через серверPresigned URL
Нагрузка на бэкендПринимает и пересылает весь файлТолько выдает подпись
Лимиты на тело запросаУпираетесь в настройки прокси и платформыРаботают лимиты хранилища
ServerlessОграничения на размер запроса и время выполненияПодходит без оговорок
Скорость для пользователяДва переходаОдин, до ближайшей точки хранилища
КонтрольПолный, но дорогойЧерез условия подписи и проверку после загрузки

Проксирование оправдано, только если файл нужно обработать до сохранения (например, антивирус в потоке) или хранилище недоступно из интернета.

Как устроен поток

  1. Клиент сообщает серверу имя, тип и размер файла.
  2. Сервер проверяет права пользователя, сам генерирует ключ объекта (не доверяя имени файла) и подписывает ссылку.
  3. Браузер загружает файл по ссылке.
  4. Клиент сообщает серверу об успешной загрузке, сервер проверяет объект (HeadObject: размер, тип) и только потом записывает его в базу.

Срок жизни и размер: presigned POST

Для presigned PUT задается срок жизни, но ограничить диапазон размера нельзя. Presigned POST позволяет вписать в политику условия, которые хранилище проверит само: диапазон размера, тип, префикс ключа. Пример на AWS SDK для JavaScript v3:

import { S3Client } from "@aws-sdk/client-s3";
import { createPresignedPost } from "@aws-sdk/s3-presigned-post";

const s3 = new S3Client({ region: process.env.S3_REGION });

export async function createUpload(userId: string, contentType: string) {
  const key = `uploads/${userId}/${crypto.randomUUID()}`;
  return createPresignedPost(s3, {
    Bucket: process.env.S3_BUCKET!,
    Key: key,
    Conditions: [
      ["content-length-range", 1, 10 * 1024 * 1024], // до 10 МБ
      ["eq", "$Content-Type", contentType],
    ],
    Fields: { "Content-Type": contentType },
    Expires: 300, // секунд
  });
}

На клиенте все поля из fields добавляются в FormData, а файл — последним:

const { url, fields } = await fetch("/api/upload").then((r) => r.json());
const form = new FormData();
Object.entries(fields).forEach(([k, v]) => form.append(k, v as string));
form.append("file", file);
await fetch(url, { method: "POST", body: form });

Срок жизни делайте коротким: ссылка нужна только на время начала загрузки. Поддержка presigned POST различается у S3-совместимых провайдеров — проверьте документацию своего. Если POST недоступен, используйте PUT и проверяйте размер после загрузки, удаляя объекты сверх лимита.

Настройка CORS

Без CORS браузер заблокирует запрос к домену хранилища. Разрешите только свои домены и нужные методы:

[
  {
    "AllowedOrigins": ["https://example.com"],
    "AllowedMethods": ["POST", "PUT"],
    "AllowedHeaders": ["*"],
    "ExposeHeaders": ["ETag"],
    "MaxAgeSeconds": 3600
  }
]

ETag нужен в ExposeHeaders, если вы используете multipart-загрузку: клиенту придется читать его из ответа на каждую часть.

Частые ошибки

  • Публичный бакет. Загрузка через подпись не требует публичного доступа. Отдавайте файлы тоже через подписанные ссылки или CDN.
  • Ключ из имени файла. Приводит к перезаписи чужих объектов и проблемам с символами. Генерируйте ключ на сервере.
  • AllowedOrigins: ["*"] в продакшене.
  • Доверие клиенту. Тип и размер из запроса — лишь заявка. Проверяйте объект после загрузки.
  • Нет очистки. Брошенные загрузки копятся. Настройте lifecycle-правило для временного префикса и для незавершенных multipart-загрузок.

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

Используйте multipart upload: файл делится на части, на каждую выдается своя подписанная ссылка, а в конце сервер завершает загрузку. Это позволяет повторять отдельные части при обрыве связи.

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.