cpp

Кроссдоменные запросы

Если мы возьмем код листинга 3.14 и отправим запрос на http://site1/ajax.php, находясь на странице http://localhost/test.html, то получим следующее сообщение об ошибке:

TypeError: NetworkError when attempting to fetch resource

Таким образом, по умолчанию мы можем отправить запрос только на тот же самый домен. Чтобы выполнить кроссдоменный запрос нужно в опции mode объекта запроса указать режим "cors" (разрешает кроссдоменные запросы; значение по умолчанию):

let url = 'http://site1/ajax.php?txt1=' + encodeURIComponent(txt1);
url += '&txt2=' + encodeURIComponent(txt2);
let response = await fetch(url, {
   mode: 'cors'
});

Указать режим недостаточно, ведь значение "cors" используется по умолчанию. Нужно дополнительно соблюсти политику CORS (Cross-Origin Resource Sharing, совместное использование ресурсов между разными источниками).

Кроссдоменные запросы делятся на два типа: простые и прочие. Простой запрос должен удовлетворять следующим основным условиям:

  • метод GET, POST или HEAD;
  • разрешены заголовки Accept, Accept-Language, Content-Language, Content-Type (со значением application/x-www-form-urlencoded, multipart/form-data или text/plain) и некоторые другие, а также заголовки, отправляемые самим Web-браузером.

При выполнении простого запроса Web-браузер дополнительно отправит заголовок Origin с названием домена:

Origin: http://localhost

Сервер должен отправить заголовок Access-Control-Allow-Origin, значением которого может быть домен из заголовка запроса Origin:

header('Access-Control-Allow-Origin: http://localhost');

или символ *, означающий все домены:

header('Access-Control-Allow-Origin: *');

В нашем примере запрос соответствует всем условиям простого запроса, поэтому в файле http://site1/ajax.php достаточно добавить вывод заголовка Access-Control-Allow-Origin, например, после вывода MIME-типа, и кроссдоменный запрос будет успешно выполнен:

// Указываем MIME-тип и кодировку
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: http://localhost');

Если запрос не является простым, то Web-браузер выполнит предварительный запрос методом OPTIONS. В этом запросе будут следующие заголовки:

  • Origin — с названием домена:
Origin: http://localhost
  • Access-Control-Request-Method — с методом запроса:
Access-Control-Request-Method: GET
  • Access-Control-Request-Headers — с названиями заголовков через запятую:
Access-Control-Request-Headers: x-requested-with

В ответ сервер должен отправить следующие заголовки:

  • Access-Control-Allow-Origin — с названием домена из заголовка запроса Origin или символом *:
$allowed_domains = array(
   'http://localhost'
);
if ( isset( $_SERVER['HTTP_ORIGIN'] ) ) {
  if ( !in_array($_SERVER['HTTP_ORIGIN'], $allowed_domains) )
     exit();
  header('Access-Control-Allow-Origin: ' .
         $_SERVER['HTTP_ORIGIN']);
}
  • Access-Control-Allow-Methods — с разрешенными методами запроса через запятую:
header('Access-Control-Allow-Methods: GET, OPTIONS');
  • Access-Control-Allow-Headers — с разрешенными заголовками через запятую:
header('Access-Control-Allow-Headers: ' .
       'X-Requested-With, Content-Type');
  • Access-Control-Max-Age — задает значение в секундах, в течение которого можно кешировать ответ на запрос OPTIONS без отправки другого запроса OPTIONS. Нельзя задать значение больше, чем разрешено Web-браузером. Укажем десять минут:
header('Access-Control-Max-Age: 600');
  • Access-Control-Allow-Credentials — если опция credentials объекта запроса имеет значение "include", то в этом заголовке нужно указать значение true. Причем в заголовке Access-Control-Allow-Origin обязательно должен быть указан домен из заголовка запроса Origin, в противном случае будет ошибка. Пример:
header('Access-Control-Allow-Credentials: true');

Если предварительный запрос выполнен успешно, то Web-браузер отправит обычный запрос, указав заголовок Origin с названием домена. Сервер должен вернуть заголовок Access-Control-Allow-Origin. Если опция credentials объекта запроса имеет значение "include", то дополнительно нужно отправить заголовок Access-Control-Allow-Credentials со значением true.

Рассмотрим пример выполнения кроссдоменного запроса. Создадим документ с формой http://localhost/test.html (листинг 3.17), который будет обмениваться данными с файлом http://site1/ajax.php (листинг 3.18). Чтобы запрос не был простым, укажем заголовок X-Requested-With. В качестве ответа получим подтверждение в формате JSON. Дополнительно произведем отправку и получение cookies.

Листинг 3.17. Содержимое файла http://localhost/test.html

<!doctype html>
<html lang="ru">
<head>
   <meta charset="utf-8">
   <meta name="viewport"
      content="width=device-width, initial-scale=1, shrink-to-fit=no">
   <link rel="stylesheet" type="text/css" href="css/bootstrap.min.css">
   <title>Кроссдоменные запросы</title>
</head>
<body>
<div class="container my-3">
  <form action="ajax.php" method="GET" onsubmit="return false;">
    <div class="form-group">
      <input type="text" class="form-control" id="txt1">
    </div>
    <button type="button" class="btn btn-primary"
            id="btnSend">Отправить методом GET</button>
  </form>
</div>
<div class="container my-3">
   <div id="div_ajax"></div>
</div>

<script>
async function sendReqGET() {
   if ( !window.fetch ) {
      window.alert('Ваш браузер не поддерживает Fetch API');
      return;
   }
   let txt1 = document.getElementById('txt1').value;
   if (txt1 === '') {
      window.alert('Не заполнено поле');
      return;
   }
   let url = 'http://site1/ajax.php?txt1=' + encodeURIComponent(txt1);
   document.getElementById('div_ajax').innerHTML = 'Загрузка...';
   try {
      let response = await fetch(url, {
         mode: 'cors',
         headers: {'X-Requested-With': 'fetch'},
         credentials: 'include'
      });
      if ( !response.ok ) {
         throw new Error('Статус: ' + response.status);
      }
      let json = await response.json();
      const msg = json.txt1 + '<br>' + json.myCookie;
      document.getElementById('txt1').value = '';
      document.getElementById('div_ajax').innerHTML = msg;
   } catch(e) {
      document.getElementById('div_ajax').innerHTML = 'Ошибка: ' + e;
   }
}
document.getElementById('btnSend').onclick = sendReqGET;
</script>
</body>
</html>

Листинг 3.18. Содержимое файла http://site1/ajax.php

<?php
$allowed_domains = array(
   'http://localhost'
);
if ( isset( $_SERVER['HTTP_ORIGIN'] ) ) {
   if ( !in_array($_SERVER['HTTP_ORIGIN'], $allowed_domains) )
      exit();
   header('Access-Control-Allow-Origin: ' .
          $_SERVER['HTTP_ORIGIN']);
   header('Access-Control-Allow-Credentials: true');
}
if ( $_SERVER['REQUEST_METHOD'] == 'OPTIONS' ) {
   header('Access-Control-Allow-Methods: GET, OPTIONS');
   header('Access-Control-Allow-Headers: ' .
          'X-Requested-With, Content-Type');
   header('Content-Type: application/json; charset=utf-8');
   header('Content-Length: 0');
   exit();
}
// Запрещаем кеширование
header('Expires: Tue, 12 May 2020 01:00:00 GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Pragma: no-cache');
// Указываем MIME-тип и кодировку
header('Content-Type: application/json; charset=utf-8');
if ( isset($_GET['txt1']) ) {
   if ( isset($_COOKIE['myCookie']) ) {
      $myCookie = $_COOKIE['myCookie'];
   }
   else $myCookie = '';
   setcookie('myCookie', time());
   $txt1 = htmlspecialchars($_GET['txt1'], ENT_COMPAT, 'UTF-8');
   $arr = array('txt1' => $txt1, 'myCookie' => $myCookie);
   echo json_encode($arr);
}
else {
   $arr = array('txt1' => 'Данные не получены', 'myCookie' => '');
   echo json_encode($arr);
}

Чтобы иметь возможность выполнять кроссдоменные запросы, при создании запроса мы передаем функции fetch() опцию mode со значением "cors". На самом деле "cors" является значением по умолчанию, поэтому опцию mode можно вообще не указывать, но явное лучше неявного. Так как мы указываем заголовок X-Requested-With, наш запрос не является простым, поэтому сначала Web-браузер отправит предварительный запрос методом OPTIONS. Чтобы иметь возможность отправлять и принимать cookies указывается опция credentials со значением "include":

let response = await fetch(url, {
   mode: 'cors',
   headers: {'X-Requested-With': 'fetch'},
   credentials: 'include'
});

Если Web-браузер отправит заголовок Origin, то на сервере будет доступна переменная окружения $_SERVER['HTTP_ORIGIN']. Мы должны вернуть заголовок Access-Control-Allow-Origin с тем же значением, что и в заголовке Origin. Символ * в данном случае указывать нельзя по двум причинам: во-первых, мы работаем с cookies, во-вторых, злоумышленник может украсть данные сессии. Поэтому, прежде чем отправить заголовок с разрешением, мы проверяем наличие домена в массиве $allowed_domains. Если домен не найден в массиве разрешенных доменов, то просто завершаем выполнение программы. Чтобы иметь возможность отправлять и принимать cookies указываем заголовок Access-Control-Allow-Credentials со значением true.

При отправке Web-браузером предварительного запроса, переменная окружения $_SERVER['REQUEST_METHOD'] будет содержать значение "OPTIONS". В этом случае мы отправляем два заголовка: Access-Control-Allow-Methods (с поддерживаемыми методами) и Access-Control-Allow-Headers (с поддерживаемыми заголовками). Отправляем еще заголовки Content-Type и Content-Length со значением 0 и завершаем выполнение программы.

Если запрос обычный, то запрещаем кеширование, а также указываем MIME-тип и кодировку данных. Если Web-браузер отправил cookies, то переменная окружения $_COOKIE['myCookie'] будет определена. Получаем ее значение и отправляем его в составе JSON-запроса. Устанавливаем cookies myCookie с помощью функции setcookie(), указывая в качестве значения число секунд, прошедшее с 1 января 1970 г., которое возвращает функция time(). Если не указать опцию credentials со значением "include", то работать с cookies мы не сможем. Далее отправляем ответ в формате JSON и получаем его в Web-браузере как обычно.

Примечание

Учебник по jQuery и AJAX
Учебник по jQuery и AJAX в формате PDF

Помощь сайту

ЮMoney (Yandex-деньги): 410011140483022

ПАО Сбербанк:
Счет: 40817810855006152256
Реквизиты банка:
Наименование: СЕВЕРО-ЗАПАДНЫЙ БАНК ПАО СБЕРБАНК
Корреспондентский счет: 30101810500000000653
БИК: 044030653
КПП: 784243001
ОКПО: 09171401
ОКОНХ: 96130
Скриншот реквизитов

cpp