Запросы на перекрестное происхождение без CORS

CORS, ты такой медленный!

Совместное использование ресурсов между источниками (CORS) - это механизм, который использует дополнительные заголовки HTTP, чтобы сообщить браузеру, что веб-приложение, работающее в одном источнике (домене), имеет разрешение на доступ к выбранным ресурсам с сервера в другом источнике. Веб-приложение выполняет HTTP-запрос с несколькими источниками, когда запрашивает ресурс, источник которого отличается от источника (домен, протокол и порт). - https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS

CORS в целом выглядит полезным инструментом, пока вы не заметите влияние на производительность. В Glia наш общедоступный REST API (с этого момента API) и наше веб-клиентское приложение (с этого момента APP) находятся в разных поддоменах. Отправка запроса из приложения в API вызовет предварительный запрос (OPTIONS). Сама по себе эта операция относительно дешевая, запрос OPTIONS отправляется только с несколькими заголовками, а сервер отвечает информацией о том, что разрешено.

В среде разработки, где все работает на одной машине, этот запрос выполняется почти мгновенно, но в мире дикой природы все обстоит иначе. На рисунке слева показано время выполнения предполетного запроса, который был отправлен из восточноевропейского местоположения на сервер, расположенный в западной части США. Этот предполетный запрос добавил почти дополнительную половину секунды задержки. Такие задержки очень заметны для нас и для наших клиентов.

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

Использовать кеширование?

Кэширование CORS очень ограничено:

  • Chrome устанавливает верхний предел для истечения срока действия кеша предпечатных запросов, который составляет 10 минут. В Firefox он установлен на 1 день.
  • Многие URL-адреса RESTful включают в себя какой-либо идентификатор ресурса. Кэширование DELETE /articles/1 по-прежнему означает, что браузер должен выполнить новый предварительный запрос при вызове DELETE /articles/2.

Отправлять только простые запросы?

Спецификация CORS говорит, что предварительные запросы не являются обязательными для простых запросов. Запрос считается простым, если он соответствует таким правилам:

  • Метод HTTP - HEAD, GET или POST.
  • Используются только заголовки Accept, Accept-Language, Content-Language и Content-Type.
  • Заголовок Content-Type установлен на application/x-www-form-urlencoded, multipart/form-data или text/plain

В нашем случае большинство наших API ожидают полезные данные JSON. Мы также используем другие методы HTTP, такие как PUT, PATCH и DELETE. Мы могли бы заставить его работать, отправив атрибут _method для маршрутизации запросов и используя тип содержимого text/plain, несмотря на то, что содержимое находится в формате JSON. Однако мы стремимся иметь чистые API, поэтому это не вариант для нас.

Избегание запросов CORS

Как обычно в мире веб-разработки, есть способы обойти ограничения, налагаемые браузерами.

document.domain

Это решение работает только, когда и сайт, и сервер используют один и тот же базовый домен. Допустим, у вас есть общедоступный API на api.example.com и сайт, ориентированный на пользователя (приложение) на app.example.com. Выполнение запроса из приложения к API вызовет предполетный запрос, поскольку субдомены разные.

Однако мы можем избежать предварительного запроса с помощью простого iframe. Сначала нам нужно предоставить новую конечную точку в нашем API, которая возвращает следующий HTML-код.

<!-- https://api.example.com/app-proxy.html -->
<!DOCTYPE HTML>
<html>
 <head>
   <script>
     document.domain = 'example.com';
     window.parent.fetch = window.fetch.bind(window);
   </script>
 </head>
</html>

Здесь следует отметить две важные вещи:

  1. Сначала устанавливаем document.domain = 'example.com'. Браузеры позволяют сменить текущий document.domain на супердомен. Например. Можно изменить foo.example.com на example.com, но нельзя изменить на bar.example.com или something.else.com.
  2. Мы заменяем функцию родительского окна fetch функцией текущего окна
    fetch.

Теперь в приложении мы можем добавить следующие две строки кода:

<script>document.domain = ‘example.com’</script>
<iframe src=”https://api.example.com/app-proxy.html"></iframe>

Сначала мы меняем текущий домен документа на example.com. Теперь и API, и приложение используют одно и то же происхождение. Это позволяет нам выполнять JavaScript между кадрами. Вторая строка включает страницу, которую мы представили в API как iframe. Это заменяет функцию текущего окна fetch функцией fetch, которая была загружена из домена API.

Теперь мы можем использовать Fetch API как обычно, чтобы делать запросы к API без
каких-либо предпечатных запросов.

Преимущества:

  • Прозрачный. Коду, использующему fetch, не нужно знать об document.domain изменениях. Сайт будет выполнять регулярные запросы с запросами предварительной проверки, если загрузка iframe не удалась.
  • Кэшируемый. HTML-код прокси-сервера не нужно изменять, за исключением случаев, когда мы хотим изменить домен, что должно происходить очень редко. Мы можем обслуживать его с сервера API с длинным кешем или даже загружать в CDN.
  • Минимальные усилия. Все, что для этого требуется, - это открыть одну статическую страницу и добавить две строки кода на сайт, доступный пользователю.

Ограничения:

  • Сайты могут иметь разные субдомены, но они должны иметь один и тот же базовый домен.
  • Включает iframe
  • Это работает только с одним доменом API. Перезапись fetch невозможна при наличии более одного домена API. Однако мы можем предоставить их как разные функции выборки (например, window.api1Fetch и window.api2Fetch).

postMessage

В подходе postMessage используются фреймы типа document.domain, но он
позволяет размещать сайты на совершенно разных доменах.

Для этого нам нужно предоставить конечную точку в API, которая обслуживает HTML-страницу - прокси. Затем мы можем использовать Window.postMessage () для отправки сообщений между приложением и прокси-страницей, которая затем отправляет их в API без предварительных запросов.

Преимущества:

  • Поддерживает разные домены. Неважно, где находится API. Весь домен может быть разным.

Ограничения:

  • Включает iframe
  • Требуется специальный адаптер для выполнения запросов. Благодаря методу document.domain мы смогли продолжать использовать Fetch API точно так же, как и раньше. Здесь нам нужен специальный адаптер, чтобы облегчить обмен данными между фреймами и гарантировать, что все работает, даже если загрузка iframe не удалась.

Ознакомьтесь с библиотекой xdomain, когда будете использовать этот подход. В настоящее время он не поддерживает Fetch API, но обычные запросы XHR подойдут.

Обратный прокси

Это решение работает, предоставляя API в текущем домене. Например. Если у вас есть API, обслуживаемый на api.example.com, а ваше приложение на app.example.com, вы можете настроить обратный прокси-сервер на app.example.com/api, который будет проксировать все запросы на api.example.com.

Преимущества:

  • Без окон iframe.
  • Поддерживает разные домены. Неважно, где находится API. Весь домен может быть разным.

Ограничения:

  • Требуется обратный прокси. Если уже существует веб-сервер (например, NGINX), обслуживающий контент приложения, это может быть тривиально.
  • Невозможно использовать, если пользовательский сайт не принадлежит нам. Например, в Глии мы предоставляем нашим клиентам JavaScript API. Неразумно заставлять каждого из наших клиентов настраивать обратный прокси-сервер, например world-largest-bank.com/glia-api, только для того, чтобы мы могли избежать междоменных запросов из нашего JS API.
  • Он изменяет URL-адреса, что может раздражать.

Прокси-сервер WebSocket

Это наиболее сложное решение. Это может быть полезным подходом, когда другие подходы невозможны или когда пользователи уже подключены к существующему серверу WebSocket.

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

Преимущества:

  • Без окон iframe.
  • Поддерживает разные домены.

Ограничения:

  • Требуется подключение к WebSocket.
  • Сложный. Требуется сервер, который умеет переводить сообщения WebSocket в HTTP-запросы.
  • Проблемы безопасности. Все прокси-запросы теперь используют IP-адрес прокси-сервера WebSocket. Это может плохо закончиться, если прокси-сервер WebSocket не заносит разрешенные домены в белый список, не ограничивает скорость и т. Д.

Заключение

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