Создавайте более отзывчивые интерфейсные приложения

Если вы разрабатываете приложение, которое позволяет пользователям загружать пользовательский контент, такой как фотографии, видео или любые другие документы, вам потребуется потратить некоторое время на создание некоторой инфраструктуры для обработки загрузки файлов.

Как правило, эти загрузки хранятся в CDN или другом хранилище, таком как Amazon S3. Хотя наиболее простым решением является загрузка файлов на внутренний сервер, а затем в хранилище, такой подход может вызвать проблемы с производительностью и другие проблемы.

В этой статье мы узнаем, как создать безопасную прямую загрузку файлов из веб-клиентов и мобильных клиентов в AWS S3, пропустив этап отправки данных на сервер. Хотя примеры кода представлены в JavaScript, как серверные, так и клиентские вызовы, этот подход может работать с любыми серверными и клиентскими технологиями, включая собственные мобильные клиентские приложения.

Загрузки через прокси-сервер против прямых клиентских загрузок

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

Кроме того, хотя конфигурация зависит от серверного программного обеспечения и хостинг-провайдера, вы часто обнаружите, что ваше приложение сталкивается с ограничениями на размер файла, когда вы пытаетесь загрузить большие файлы. Например, у популярного сервера nginx, используемого для "внешнего" доступа ко многим веб-сервисам, ограничение загрузки по умолчанию составляет 1 МБ. Это значение можно увеличить, но это может быть вне вашего контроля как разработчика приложения, и даже большее значение может оказаться слишком маленьким для вашего приложения.

Прямая загрузка, напротив, загружает ваш контент непосредственно в хранилище, минуя необходимость загрузки контента на ваш собственный сервер. Это звучит просто, но на практике это немного сложно! Есть несколько дополнительных соображений для безопасной прямой загрузки.

Соображения безопасности прямой клиентской загрузки

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

Задача состоит в том, чтобы дать клиенту разрешение на безопасную загрузку файла. Если мы попытаемся использовать те же учетные данные, которые мы используем на бэкэнде, это означает, что они проходят через клиент и могут быть перехвачены злоумышленником. Даже если мы создадим отдельные ключи с ограниченным доступом для клиента, злоумышленник все равно сможет загрузить произвольный контент, что позволит ему заполнить хранилище, выполнить DOS-атаки и т. д. Единственное безопасное решение — позволить клиенту загружать только разрешенный нами контент.

По сути, нам нужен одноразовый ключ, который позволяет напрямую загружать файл, который мы разрешаем. Как оказалось, S3 (и другие поставщики хранилищ) позволяют нам генерировать что-то подобное в виде предварительно подписанного URL-адреса. Этот URL-адрес ссылается на наш объект S3 с добавленным параметром подписи. Этот специальный URL-адрес позволяет нам писать в объект, на который ссылаются, и только на объект, на который ссылаются.

Реализация прямой загрузки клиентов

Наш подход к прямой загрузке клиентов будет использовать предварительно подписанные URL-адреса, чтобы клиент мог загружать напрямую в наш поставщик хранилища. Конечно, для создания предварительно подписанного URL-адреса требуются предоставленные службой учетные данные с достаточными учетными данными, поэтому подпись не может быть выполнена на клиенте. Вместо этого мы попросим сервер проверить загруженные метаданные (проверить авторизацию пользователя, тип файла, размер и т. д.), сгенерировать подписанный URL-адрес и вернуть его клиенту.

На изображении ниже показана разница в потоке между загрузкой через прокси-сервер и прямой загрузкой клиента:

Вот основные шаги для создания безопасной прямой загрузки клиента с заранее заданным URL-адресом:

  • Реализовать обработчик ввода файла в клиенте для получения файла от пользователя.
  • Отправьте метаданные файла на сервер для проверки запроса.
  • Создайте предварительно подписанный URL-адрес загрузки на сервере и верните его клиенту.
  • Сделайте запрос PUT от клиента, чтобы загрузить файл на S3.

Создание подписанного URL-адреса на стороне сервера

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

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

Мы реализуем сервис на нашем бэкэнде для получения URL-адреса загрузки с помощью AWS SDK:

import AWS from "aws-sdk"   

const s3 = new AWS.S3() 
const getUploadURL = async (bucket: string, path: string, contentType: string) => { 
  let putURL = await s3.getSignedUrlPromise('putObject', { 
    Bucket: bucket, 
    Key: path, 
    Expires: 120, 
    ContentType: contentType 
  })
 
  return putURL 
}

Бэкенд-код для создания предварительно подписанного URL-адреса S3.

Эта функция генерирует наш подписанный URL. Далее мы напишем быстрый сервис для раскрытия функциональности. Например, используя веб-сервер Express, мы могли бы написать сервис следующим образом:

app.post("/upload_url", async (req: Request, res: Response) => { 
  let url = await uploadURL(userUploadBucket, req.body["path"], req.body["content_type"]) 
  res.send({ put_url: url }) 
})

Реализация загрузки на стороне клиента

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

Обычно мы реализуем это в обработчике onChange ввода файла, чтобы он срабатывал, когда пользователь выбирает или перетаскивает файл.

const handler = (e: ChangeEvent<HTMLInputElement>) => { 
  const files = e?.target?.files

  if (files && files.length > 0) { 
    // fetch the upload URL
    let response = await fetch(`${serverURL}/upload_url`, { 
      method: 'POST', 
      headers: { 'Content-Type': 'application/json' }, 
      body: JSON.stringify({ directory, extension, content_type: contentType }) 
    }) 

    let json = await response.json() 

    // perform the direct upload
    let uploadResponse = await fetch(json.url, { method: "PUT", body: content })

    // handle completion/errors, update state to display uploaded file, etc 
  } 
}

Тада! Имея этот код, мы можем выполнять загрузку напрямую в S3, не отправляя их сначала на внутренний сервер.

Последний улов! Отображение загруженных файлов

Хотя наша реализация загрузки может быть завершена, функция, вероятно, еще не реализована! В большинстве веб-приложений загруженный файл отображается пользователю после загрузки содержимого. Самый простой способ сделать это — использовать только что загруженный URL-адрес. Загвоздка в том, что наш сгенерированный URL загрузки не позволяет нам просматривать объект! Вместо этого нам нужно сгенерировать другой URL-адрес, который будет использоваться для просмотра объекта после его загрузки. Не волнуйтесь, это просто! У нас просто есть наш оригинальный сервис, возвращающий два URL-адреса:

const uploadURL = async (bucket: string, path: string, contentType: string) => { 
  let getURL = s3.getSignedUrl('getObject', { 
    Bucket: bucket, 
    Expires: 60 * 60 * 24 * 7, 
    Key: path 
  }); 
  
  let putURL = await s3.getSignedUrlPromise('putObject', { 
    Bucket: bucket, 
    Key: path, 
    Expires: 120, 
    ContentType: contentType 
  }) 

  return { getURL, putURL } 
}

Резюме — более быстрая загрузка для веб-приложений и мобильных приложений 🎉

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

Первоначально опубликовано на https://blixtdev.com 6 марта 2023 г.