CORS с Symfony, jQuery, FOSRestBundle и NelmioCorsBundle

⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
TL; DR: Изменить (перед прочтением всего):

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

⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️

(конец примечания)

Ситуация

У меня есть многодоменное приложение Symfony, и по некоторым причинам задняя часть обновляет данные с помощью веб-сервисов, а не классических форм.

Back-end: back.mydomain.dev Домен веб-сервисов: api.mydomain.dev

Я использую jQuery для вызова AJAX для этих веб-сервисов, и если я хочу изменить или создать объекты, я также отправляю запросы AJAX, объекты объединяются с объектами Doctrine и сохраняются.

Я целый год боролся за то, чтобы сделать GET, PUT, POST и DELETE запросы, которые будут работать должным образом в этом приложении, и только потому, что они находятся в разных доменах, мне приходится настраивать CORS в разных средах.

Настройка

Все запросы jQuery AJAX выглядят следующим образом:

ajaxObject = {
    url: 'http://api.mydomain.dev/' + uri,
    type: method, // Can be GET, PUT, POST or DELETE only
    dataType: 'json',
    xhrFields: {
         withCredentials: true
    },
    crossDomain: true,
    contentType: "application/json",
    jsonp: false,
    data: method === 'GET' ? data : JSON.stringify(data) // Here, "data" is ALWAYS containing a plain object. If empty, it equals to "{}"
};

// ... Add callbacks depending on requests

$.ajax(ajaxObject);

Позади, маршруты управляются с помощью Symfony.

Для конфигурации CORS я использую NelmioCorsBundle с этой конфигурацией:

nelmio_cors:
    paths:
       "^/":
          allow_credentials: true
          origin_regex: true
          allow_origin:
              - "^(https?://)?(back|api)\.mydomain.dev/?"
          allow_headers: ['Origin','Accept','Content-Type']
          allow_methods: ['POST','GET','DELETE','PUT','OPTIONS']
          max_age: 3600
          hosts:
              - "^(https?://)?(back|api)\.mydomain.dev/?"

Используемый контроллер расширяет FOSRestBundle один, имеет некоторую безопасность (например, не может POST/PUT/DELETE, когда вы этого не делаете имеют правильную роль), могут обновлять объекты и возвращать только данные JSON (для этого есть слушатель).

Идеальное поведение

В идеале я хочу:

  • Запустите запрос jQuery AJAX POST/PUT/DELETE
  • Он должен отправить запрос OPTIONS со всеми заголовками CORS
  • NelmioCorsBundle должен вернуть правильные заголовки CORS, принимающие запрос, который должен быть выполнен, даже перед запуском любого контроллера внутри приложения (сделанного слушателем запроса на соединение)
  • Если это принято, соответствующий HTTP-запрос отправляется контроллеру со всеми данными запроса как последовательная строка JSON (в полезной нагрузке запроса), а Symfony извлекает его и интерпретирует правильную строку JSON как объект массива
  • Затем контроллер получает данные, выполняет свои действия и возвращает ответ application/json.

Проблема

Но здесь я попробовал комбинации maaaaany, и я не могу заставить его работать.

Точка n ° 3 и 4 терпит неудачу, полностью или частично.

Проработанные обходные пути

  • Когда я не сериализую data с JSON.stringify (см. выше Настройка), запрос OPTIONS отправляется, но FOSRestBundle отправляет BadRequestHttpException сообщение Invalid json message received, и это абсолютно нормально, потому что "полезная нагрузка запроса" (как видно из инструментов разработчика Chrome) является классическим контентом application/x-www-form-urlencoded, даже если я указал contentType: "application/json" в запросе jQuery AJAX, тогда как это должна быть сериализованная строка JSON.

  • Однако, если я сделать сериализует data var, "полезная нагрузка запроса" действительна, но запрос OPTIONS не отправляется, что приводит к сбою всего запроса из-за отсутствия принятия CORS.

  • Если заменить contentType: "application/json" на application/x-www-form-urlencoded или multipart/form-data и не сериализовать данные, тогда полезная нагрузка запроса будет действительна, но запрос OPTIONS не будет отправлен. Это также должно быть нормальным, как описано в jQuery docs в параметре contentType:

    Примечание.. Для междоменных запросов установка типа содержимого для чего-либо, кроме приложения /x -www-form-urlencoded, multipart/form-data или text/plain вызовет браузер для отправки на сервер запроса опций OPTIONS.

    Но тогда, как отправить правильный запрос CORS?

Вопросы

  • Откуда возникает эта проблема?
  • Это проблема с моей настройкой? С jQuery?
  • С самим AJAX?
  • Какими могут быть решения для решения этой проклятой проблемы?

Редактирование (после комментариев)

2015-06-29 17:39

Условия:

В AJAX параметр contentType установлен на application/json. Параметр data установлен в сериализованную строку JSON.

Заголовки предварительных полей REQUEST

OPTIONS http://api.mydomain.dev/fr/object/1 HTTP/1.1
Access-Control-Request-Method: POST
Origin: http://back.mydomain.dev
Access-Control-Request-Headers: accept, content-type
Referer: http://back.mydomain.dev/...

Предполненные заголовки RESPONSE

HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: POST, GET, DELETE, PUT, OPTIONS
Access-Control-Allow-Headers: origin, accept, content-type
Access-Control-Max-Age: 3600
Access-Control-Allow-Origin: http://back.mydomain.dev

POST Заголовки REQUEST (после предполета)

POST http://api.mydomain.dev/fr/object/1 HTTP/1.1
Origin: http://back.mydomain.dev
Content-Type: application/json
Referer: http://back.mydomain.dev/...
Cookie: mydomainPortal=v5gjedn8lsagt0uucrhshn7ck1
Accept: application/json, text/javascript, */*; q=0.01

Запрос полезной нагрузки (необработанный): {"json":{"id":1,"name":"object"}}

Следует отметить, что это вызывает ошибку javascript:

XMLHttpRequest не может загрузить http://api.mydomain.dev/fr/object/1. В запрошенном ресурсе нет заголовка "Access-Control-Allow-Origin". Происхождение http://back.mydomain.dev ', следовательно, не допускается.

POST Заголовки RESPONSE (после предполета)

HTTP/1.1 200 OK
Date: Mon, 29 Jun 2015 15:35:07 GMT
Server: Apache/2.4.12 (Unix) OpenSSL/1.0.2c Phusion_Passenger/5.0.11
Keep-Alive: timeout=5, max=91
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: text/html; charset=UTF-8

2015-06-29 17:39

Я также попытался использовать vanilla javascript для создания XMLHttpRequest, проблема все та же:

var xhr = new XMLHttpRequest;
xhr.open('POST', 'http://api.mydomain.dev/fr/objects/1', true);
xhr.setRequestHeader('Content-type', 'application/json');
xhr.withCredentials = true; // Sends cookie
xhr.onreadystatechange = function(e) {
    // A simple callback
    console.info(JSON.parse(e.target.response));
};
// Now send the request with the serialized payload:
xhr.send('{"id":1,"name":"Updated test object"}');

Затем браузер отправляет запрос OPTIONS:

OPTIONS http://api.mydomain.dev/fr/objects/1 HTTP/1.1
Connection: keep-alive
Access-Control-Request-Headers: content-type
Access-Control-Request-Method: POST
Origin: http://back.mydomain.dev

Что возвращает правильные заголовки ответов:

Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: POST, GET, DELETE, PUT, OPTIONS
Access-Control-Allow-Headers: origin, accept, content-type
Access-Control-Max-Age: 3600
Access-Control-Allow-Origin: http://back.mydomain.dev

Но тогда браузер отправляет запрос POST без заголовка "Access-Control- *".

Ответы

Ответ 1

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

Это была просто какая-то... Глупость.

An exit; был скрыт глубоко в файле PHP.

Извините за раздражение. Я предполагаю, что это какая-то запись в соотношении текст/качество на SO.

Изменить: Я все еще надеюсь, что эта проблема может быть хорошим примером полностью совместимого с CORS проекта Symfony.