Статьи

Углубленный взгляд на CORS

Эта статья была рецензирована Panayiotis «pvgr» Velisarakos . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!

CORS — это относительно новый API, который поставляется с HTML5, который позволяет нашим веб-сайтам запрашивать внешние и ранее ограниченные ресурсы. Это ослабляет традиционную политику того же происхождения , позволяя нам запрашивать ресурсы, которые находятся в другом домене, чем наша родительская страница.
Например, до междоменной области CORS запросы Ajax были невозможны (вызов Ajax со страницы example.com/index.html на anotherExample.com/index.html ).

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

CORS и поддержка браузера

Internet Explorer 8 и 9 поддерживают CORS только через класс XDomainRequest . Основное отличие состоит в том, что вместо обычного создания экземпляра с чем-то вроде var xhr = new XMLHttpRequest() вам придется использовать var xdr = new XDomainRequest(); ,
IE 11, Edge и все последние и не очень свежие версии Firefox, Safari, Chrome, Opera полностью поддерживают CORS. IE10 и браузер Android по умолчанию до 4.3 не поддерживают CORS только при использовании для изображений в элементах <canvas> .

Согласно CanIuse, 92,61% людей во всем мире поддерживают браузеры, что указывает на то, что мы, вероятно, не допустим ошибки, если будем ее использовать.

Создание простого Ajax-запроса перекрестного происхождения

Теперь, когда мы знаем, что политика одного и того же происхождения запрещает веб-сайтам в разных доменах отправлять запросы Ajax другим доменам, давайте посмотрим, как мы можем обойти это, чтобы сделать запрос Ajax перекрестного происхождения на другой веб-сайт.

Если вы просто попытаетесь отправить запрос Ajax на случайный веб-сайт, он, скорее всего, не сможет прочитать ответ, если это не разрешено другим веб-сайтом.

 <script> var xhr = new XMLHttpRequest(); var url = "//example.com"; xhr.open("GET", url); xhr.onreadystatechange = function() { if (xhr.status === 200 && xhr.readyState === 4) { document.querySelector("body").innerHTML = xhr.responseText } } xhr.send(); </script> 

Если вы откроете консоль вашего браузера, вы получите сообщение, похожее на:
XMLHttpRequest не может загрузить http://example.com . В запрошенном ресурсе отсутствует заголовок «Access-Control-Allow-Origin». Поэтому происхождение ‘ http://otherExampleSite.com ‘ не разрешено.

Чтобы успешно прочитать ответ, вы должны установить заголовок с именем Access-Control-Allow-Origin . Этот заголовок должен быть установлен либо в apache.conf -логике вашего приложения (устанавливая заголовок вручную перед доставкой ответа клиенту), либо в конфигурации вашего сервера (например, редактирование apache.conf и добавление Header set Access-Control-Allow-Origin "*" к нему, если вы используете Apache).

Добавление заголовка с метатегом в тег <head> вашего документа, как это не будет работать: <meta http-equiv="Access-Control-Allow-Origin" content="*">

Вот как вы можете включить перекрестные запросы для всех источников (сайтов, которые запрашивают ресурс) в PHP:

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

Если вы хотите разрешить конкретное происхождение, вы можете сделать что-то вроде этого в PHP:

 header("Access-Control-Allow-Origin: http://example.com"); 

Однако сам заголовок Access-Control-Allow-Origin не позволяет вставлять в заголовок несколько хостов, независимо от разделителя. Это означает, что если вы хотите разрешить перекрестный запрос из разных доменов, вы должны динамически генерировать заголовок.

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

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

Для упрощения предположим, что у нас есть два веб-сайта. Первый устанавливает cookie и каждый раз, когда пользователь входит, показывает значение cookie, которое должно быть его именем. Другой веб-сайт делает перекрестный запрос Ajax и добавляет ответ в свой DOM.

Мы устанавливаем куки с именем пользователя и отображаем его при каждом посещении

Мы видим, что веб-сайт, отправляющий Ajax-запрос с несколькими источниками, действительно получает представление страницы по умолчанию без персонализации из-за файлов cookie и сеансов.

Получение страницы, как видит пользователь с помощью CORS

Если мы хотим включить учетные данные пользователя в удаленный запрос, мы должны сделать два изменения: первое в коде веб-сайта, выполняющего запрос, и второе в веб-сайте, получающем запрос. На веб-сайте, выполняющем запрос, мы должны установить для свойства withCredentials запроса Ajax значение true :

 var xhr = new XMLHttpRequest(); xhr.withCredentials = true; 

Сам удаленный сервер, помимо разрешения нашего происхождения, должен установить заголовок Access-Control-Allow-Credentials и установить его значение в true . Использование чисел 1 или 0 не будет работать.

Если мы просто установим withCredentials в значение true но сервер не установил вышеупомянутый заголовок, мы не получим ответ, даже если наш источник разрешен. Мы получим сообщение, похожее на:
XMLHttpRequest не может загрузить http://example.com/index.php . Флаг Credentials имеет значение «true», но заголовок «Access-Control-Allow-Credentials» имеет значение «. Это должно быть 'true', чтобы разрешить учетные данные. Поэтому происхождение ' http: // localhost: 63342 ' не разрешено.

Если оба изменения будут внесены, мы получим персональный ответ. В нашем случае имя пользователя, которое мы сохранили в cookie, будет в ответе, который удаленный сервер возвращает на наш сайт.

Мы получаем ответ от удаленного сервера на основании запроса с переданными учетными данными пользователя. Другими словами, мы получаем страницу так, как ее видит пользователь.

Однако разрешить передачу учетных данных в запрос кросс-источника довольно опасно, поскольку это открывает возможность для различных атак, таких как CSRF (подделка межсайтового запроса), XSS (межсайтовый скриптинг) и злоумышленник может принять преимущество входа пользователя в систему для выполнения действий на удаленном сервере без ведома пользователя (например, снятие денег, если удаленный сервер является банковским веб-сайтом).

Preflights

Когда запросы начинают усложняться, мы можем захотеть узнать, разрешен ли и принят ли конкретный метод запроса (такой как get , put , post , patch или delete ) или конкретный пользовательский заголовок и принят ли сервер. В этом случае вы можете использовать предварительные рейсы, когда вы сначала отправляете запрос с помощью метода options и объявляете, какой метод и заголовки будут иметь ваш запрос. Затем, если сервер возвращает заголовки CORS и мы видим, что наши origin, заголовки и методы запроса разрешены, мы можем сделать фактический запрос (Origin - это заголовок, который передается нашими браузерами при каждом запросе перекрестного источника, который мы делаем. И нет, мы не можем изменить значение Origin при запросе в обычном браузере).

Ответ на метод OPTIONS в предварительном запросе

Как мы видим на изображении выше, сервер возвращает несколько заголовков, которые мы можем использовать, чтобы определить, следует ли сделать фактический запрос. Он возвращает нам, что все источники разрешены ( Access-Control-Allow-Origin: * , что мы не можем сделать запрос при передаче учетных данных пользователя ( Access-Control-Allow-Credentials ), что мы можем только делать запросы get ( Access-Control-Allow-Methods ) и что мы можем использовать пользовательский заголовок X-ApiKey ( Access-Control-Allow-Headers ). Наконец, Access-Control-Allow-Headers Access-Control-Max-Age показывают значение в секундах, указывая, как долго (с время запроса) мы можем делать запросы, не полагаясь на другой предполетный полет.

С другой стороны, в нашей внешней логике мы передаем Access-Control-Request-Method и передаем Access-Control-Request-Headers чтобы указать, какой метод запроса и какие заголовки мы намереваемся добавить в наш реальный запрос. В Vanilla JavaScript вы можете прикрепить заголовок при выполнении вызовов Ajax с помощью xhr.setRequestHeader ('headerString', 'headerValueString'); ,

CORS для холст изображения

Если мы хотим загрузить внешние изображения и отредактировать их на холсте или просто сохранить их значение в кодировке base64 в localStorage в качестве механизма кэширования, удаленный сервер должен включить CORS. Есть разные способы сделать это. Один из способов - это изменить конфигурацию вашего веб-сервера, добавив заголовок Access-Control-Allow-Origin при каждом запросе для определенных типов изображений, такой пример показан в документации Mozilla . Если у нас есть скрипт, который динамически генерирует изображения путем изменения типа Content-Type и выводит изображение, например http://example.com/image/image.php?image=1, мы можем просто установить этот заголовок вместе с выводом изображения.
Без CORS, если мы попытаемся получить доступ к удаленному изображению, загрузить его на холст, отредактировать и сохранить его с помощью toDataURL или просто попытаться добавить измененное изображение в DOM с помощью toDataURL , мы получим следующее исключение безопасности (и мы не будем сохранить или показать его): изображение из источника " http://example.com " заблокировано для загрузки политикой общего доступа к ресурсам разных источников: в запрошенном заголовке «Access-Control-Allow-Origin» отсутствует ресурс. Поэтому происхождение ' http: // localhost: 63342 ' не разрешено .
Если сервер, на котором находится изображение, возвращает изображение вместе с заголовком Access-Control-Allow-Origin: * , то мы можем сделать следующее:

 var img = new Image, canvas = document.createElement("canvas"), ctx = canvas.getContext("2d"), src = "http://example.com/test/image/image.php?image=1"; img.setAttribute('crossOrigin', 'anonymous'); img.onload = function() { canvas.width = img.width; canvas.height = img.height; ctx.drawImage( img, 0, 0 ); ctx.font = "30px Arial"; ctx.fillStyle = "#000"; ctx.fillText("#SitePoint",canvas.width / 3,canvas.height / 3); img.src = canvas.toDataURL(); document.querySelector("body").appendChild(img); localStorage.setItem( "savedImageData", canvas.toDataURL("image/png") ); } img.src = src; 

Это загрузит внешнее изображение, добавит в него текст #SitePoint, отобразит его для пользователя и сохранит в localStorage. Обратите внимание, что мы установили атрибут crossOrigin внешнего изображения - img.setAttribute('crossOrigin', 'anonymous'); , Этот атрибут является обязательным, и если мы не добавим его во внешний образ, мы все равно получим еще одно исключение безопасности.

Корс-Са

Атрибут Crossorigin

Когда мы делаем запросы на внешние изображения, аудио, видео, таблицы стилей и скрипты, используя соответствующий тег HTML (5), мы не делаем запрос CORS. Это означает, что заголовок Origin не отправляется на страницу, обслуживающую внешний ресурс. Без CORS мы не смогли бы редактировать внешнее изображение на холсте, просматривать исключения и вести журнал ошибок из внешних скриптов, загружаемых нашим веб-сайтом, или использовать объектную модель CSS при работе с внешними таблицами стилей и так далее. Есть определенные случаи, когда мы хотим использовать эти функции, и здесь crossorigin атрибут crossorigin который мы упомянули выше.

crossorigin может быть установлен для таких элементов, как <link> , <img> и <script> . Когда мы добавляем атрибут к такому элементу, мы гарантируем, что запрос CORS будет выполнен с правильно установленным заголовком Origin . Если внешний ресурс разрешает ваше происхождение через заголовок Access-Control-Allow-Origin ограничения на запросы не-CORS не будут применяться.

crossorigin имеет два возможных значения:

  1. anonymous - установка атрибута crossorigin в это значение сделает запрос CORS без передачи учетных данных пользователя во внешний ресурс (аналогично выполнению запроса Ajax CORS без добавления атрибута withCredentials ).
  2. use-credentials - установка атрибута crossorigin в это значение сделает CORS-запрос к внешнему ресурсу вместе с любыми учетными данными пользователя, которые могут существовать для этого ресурса. Чтобы это работало, сервер должен не только установить заголовок Access-Control-Allow-Origin который разрешает ваш источник, но также должен установить для Access-Control-Allow-Credentials значение true .

К учетным данным пользователя относятся файлы cookie, учетные данные HTTP Basic Auth, сертификаты и другие пользовательские данные, которые отправляются, когда пользователь запрашивает определенный веб-сайт.

Выводы

CORS позволяет разработчикам в дальнейшем взаимодействовать с другими системами и веб-сайтами, чтобы создавать еще лучшие веб-интерфейсы. Он может использоваться вместе с традиционными запросами, сделанными популярными тегами HTML, а также с технологиями Web 2.0, такими как Ajax.

Вы использовали CORS в своих проектах? У вас были трудности с этим? Мы хотели бы знать, каковы ваши впечатления от этого.

Ссылки и дальнейшее чтение: