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.