Запросы на перекрестное происхождение без 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>
Здесь следует отметить две важные вещи:
- Сначала устанавливаем
document.domain = 'example.com'
. Браузеры позволяют сменить текущий document.domain на супердомен. Например. Можно изменитьfoo.example.com
наexample.com
, но нельзя изменить наbar.example.com
илиsomething.else.com
. - Мы заменяем функцию родительского окна
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, но они не очень эффективны. Наилучший вариант - по возможности полностью избегать предполетных запросов.