symfony3 - CORS con Symfony, jQuery, FOSRestBundle y NelmioCorsBundle
reason cors header access control allow origin missing symfony (1)
⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
TL; DR: Editar (antes de leer todo):
Este problema está resuelto porque cometí un error muy estúpido en mi código, no relacionado con CORS ni nada.
Si desea leer este tema de todos modos, solo tenga en cuenta que tiene una configuración de CORS que funciona , si es posible que desee tomar un buen ejemplo.
⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
(fin de la nota)
Situación
Tengo una aplicación Symfony multidominio, y la parte de back-end actualiza datos con servicios web, no formularios clásicos, por alguna razón.
Back-end: back.mydomain.dev Webservices domain: api.mydomain.dev
Estoy usando jQuery para hacer llamadas AJAX a estos servicios web, y si quiero modificar o crear objetos, también envío solicitudes AJAX, los objetos se fusionan con entidades de Doctrine y persisten.
He estado luchando durante todo un año para hacer solicitudes GET, PUT, POST y DELETE que funcionen correctamente en esta aplicación, y solo porque están en dominios diferentes, me veo obligado a configurar CORS en mis diferentes entornos.
Preparar
Todas las solicitudes de jQuery AJAX se ven así:
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);
Detrás, las rutas se administran con Symfony.
Para la configuración de CORS , estoy usando NelmioCorsBundle con esta configuración:
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/?"
El controlador utilizado está extendiendo el de FOSRestBundle , tiene algo de seguridad (por ejemplo, no puede POST / PUT / DELETE cuando no tiene el rol correcto), puede actualizar objetos y devuelve solo datos JSON (hay un detector para eso) .
Comportamiento ideal
Idealmente, quiero esto:
- Ejecute la solicitud jQuery AJAX POST / PUT / DELETE
- Tiene que enviar una solicitud OPTIONS con todos los encabezados CORS
- NelmioCorsBundle debe devolver los encabezados CORS correctos aceptando la solicitud que debe realizarse, incluso antes de ejecutar cualquier controlador dentro de la aplicación (realizada por el oyente de solicitud del paquete)
- Si se acepta, la solicitud HTTP adecuada se envía al controlador con todos los datos de solicitud como una cadena JSON serializada (en la carga útil de la solicitud), y Symfony la recupera e interpreta la cadena JSON correcta como un objeto de matriz
- El controlador obtiene los datos, hace sus cosas y devuelve una respuesta de
application/json
.
Problema
Pero aquí, probé todas las combinaciones, y parece que no puedo hacer que funcione.
Los puntos n ° 3 y 4 están fallando, total o parcialmente.
Intentó soluciones
Cuando no serializo los
data
conJSON.stringify
(vea la parte de Configuración anterior), se envía la solicitud OPTIONS, pero FOSRestBundle envía unaBadRequestHttpException
diceInvalid json message received
, y es totalmente normal porque la "carga útil de solicitud" (como se ve) en las herramientas para desarrolladores de Chrome) es el contenido clásicoapplication/x-www-form-urlencoded
, incluso si especifiquécontentType: "application/json"
en la solicitud jQuery AJAX, mientras que debería ser una cadena JSON serializada.Sin embargo, si serializo la var de
data
, la "carga útil de solicitud" es válida, pero la solicitud de OPCIONES no se envía, lo que hace que toda la solicitud falle debido a la falta de aceptación de CORS.Si reemplazo
contentType: "application/json"
conapplication/x-www-form-urlencoded
omultipart/form-data
, y no serializo los datos, entonces, la carga útil de la solicitud es válida, pero la solicitud OPTIONS no se envía . También debería ser normal, como se explica en los documentos de jQuery en el parámetro contentType :Nota: Para las solicitudes de dominio cruzado, establecer el tipo de contenido en cualquier otra cosa que no sea application / x-www-form-urlencoded, multipart / form-data, o text / plain activará el navegador para enviar una solicitud de OPCIONES previas al servidor.
Pero entonces, ¿cómo enviar una solicitud CORS correcta?
Preguntas
- ¿De dónde viene este problema?
- ¿Es esto un problema con mi configuración? Con jQuery?
- Con AJAX en sí?
- ¿Cuáles pueden ser las diferentes soluciones para resolver este maldito problema?
Edits (después de los comentarios)
2015-06-29 17:39
Condiciones:
En AJAX, contentType
se establece en application/json
. La opción de data
se establece en una cadena JSON serializada.
Encabezados REQUEST de Preflight
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/...
Jefes de RESPUESTA antes del vuelo
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 encabezados (después de la verificación previa)
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
Carga de solicitud (en bruto): {"json":{"id":1,"name":"object"}}
Debe señalarse que esto arroja un error de javascript:
XMLHttpRequest no puede cargar http://api.mydomain.dev/fr/object/1 . Ningún encabezado ''Access-Control-Allow-Origin'' está presente en el recurso solicitado. El origen '' http://back.mydomain.dev '' no está, por lo tanto, permitido.
POST
RESPONSE encabezados (después de la verificación previa)
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
También traté de usar vainilla javascript para hacer XMLHttpRequest, el problema sigue siendo el mismo:
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"}'');
Luego, el navegador envía una solicitud de OPCIONES:
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
Que devuelve los encabezados de respuesta correctos:
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
Pero luego, el navegador envía la solicitud POST sin ningún encabezado "Access-Control- *".
En realidad, mi configuración funcionó bien.
Absolutamente perfecto, si pudiera decirlo.
Era solo una especie de ... Estupidez.
Una exit;
estaba escondido en un archivo PHP.
Lo siento por molestia. Supongo que es una especie de registro en una proporción de texto / calidad en SO.
Editar: todavía espero que este problema pueda ser un ejemplo adecuado de un proyecto Symfony totalmente compatible con CORS.