CORS
Com configurar CORS a Laravel: orígens permesos, credencials, peticions preflight, SPAs i depuració d'errors.
Què és CORS?#
CORS (Cross-Origin Resource Sharing) és un mecanisme de seguretat dels navegadors que controla quins dominis poden fer peticions a la teva API. Per defecte, els navegadors bloquegen les peticions JavaScript que van a un domini diferent del que ha servit la pàgina web. Això és la "same-origin policy" (política del mateix origen) i existeix per prevenir que un lloc web maliciós faci peticions a altres llocs en nom de l'usuari.
Un "origen" es defineix per la combinació de protocol, domini i port. Per exemple, https://app.example.com i https://api.example.com són orígens diferents (domini diferent). http://localhost:3000 i http://localhost:8000 també són orígens diferents (port diferent). Fins i tot http://example.com i https://example.com són orígens diferents (protocol diferent).
Sense CORS, una SPA a https://frontend.example.com no podria fer peticions AJAX a una API a https://api.example.com. El navegador bloquejaria la petició abans que arribi al servidor. CORS permet al servidor indicar explícitament quins orígens tenen permís per accedir als seus recursos, relaxant la same-origin policy de manera controlada.
Cal entendre que CORS és una restricció del navegador, no del servidor. Si fas una petició amb curl, Postman o des del backend (PHP, Node.js), CORS no s'aplica. Només afecta les peticions fetes amb JavaScript des d'un navegador (XMLHttpRequest, Fetch API). Això significa que CORS no és un mecanisme de seguretat del servidor: és un mecanisme que protegeix l'usuari del navegador contra llocs web maliciosos.
Com funciona CORS#
Quan el navegador detecta una petició cross-origin, segueix un protocol de negociació amb el servidor:
Peticions simples#
Les peticions simples (GET, HEAD, POST amb content types bàsics) s'envien directament al servidor. El servidor respon amb capçaleres CORS (Access-Control-Allow-Origin) que el navegador verifica. Si l'origen no està permès, el navegador bloqueja la resposta (el servidor ja ha processat la petició, però el JavaScript no pot llegir la resposta):
Navegador → Servidor:
GET /api/articles HTTP/1.1
Host: api.example.com
Origin: https://frontend.example.com
Servidor → Navegador:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://frontend.example.com
(El navegador veu que l'origen coincideix i permet que JavaScript llegeixi la resposta)
Peticions preflight#
Les peticions més complexes (PUT, DELETE, peticions amb capçaleres personalitzades, peticions amb Content-Type: application/json) requereixen un pas addicional: la petició preflight. Abans d'enviar la petició real, el navegador envia una petició OPTIONS per preguntar al servidor si la petició real està permesa:
Navegador → Servidor (preflight):
OPTIONS /api/articles/1 HTTP/1.1
Host: api.example.com
Origin: https://frontend.example.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Authorization, Content-Type
Servidor → Navegador (resposta preflight):
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://frontend.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 3600
(El navegador veu que DELETE està permès i envia la petició real)
Navegador → Servidor (petició real):
DELETE /api/articles/1 HTTP/1.1
Host: api.example.com
Origin: https://frontend.example.com
Authorization: Bearer token...
La capçalera Access-Control-Max-Age indica al navegador quant de temps (en segons) pot guardar en cache la resposta preflight. Això evita fer una petició OPTIONS abans de cada petició real, millorant el rendiment.
Configuració a Laravel#
Laravel gestiona CORS a través del fitxer config/cors.php. Aquesta configuració s'aplica automàticament a totes les rutes que coincideixin amb els patrons definits:
// config/cors.php
return [
// Rutes on s'aplica CORS
'paths' => ['api/*', 'sanctum/csrf-cookie'],
// Mètodes HTTP permesos
'allowed_methods' => ['*'],
// Orígens permesos
'allowed_origins' => [],
// Patrons d'orígens (regex)
'allowed_origins_patterns' => [],
// Capçaleres que el client pot enviar
'allowed_headers' => ['*'],
// Capçaleres que el client pot llegir de la resposta
'exposed_headers' => [],
// Temps de cache del preflight (en segons)
'max_age' => 0,
// Permet enviar cookies/credencials
'supports_credentials' => false,
];Orígens permesos#
La configuració d'orígens és l'aspecte més important de CORS. Defineix quins dominis poden fer peticions a la teva API:
// Permetre orígens específics (recomanat per producció)
'allowed_origins' => [
'https://frontend.laravelandorra.com',
'https://app.laravelandorra.com',
'https://admin.laravelandorra.com',
],
// Permetre tots els orígens (només per desenvolupament)
'allowed_origins' => ['*'],
// Patrons amb regex (útil per subdominis dinàmics)
'allowed_origins_patterns' => [
'/^https:\/\/.*\.laravelandorra\.com$/',
],En producció, sempre especifica els orígens exactes en lloc d'usar *. El wildcard * permet que qualsevol lloc web faci peticions a la teva API, cosa que pot ser un risc de seguretat si l'API retorna dades sensibles.
Configuració per entorn#
Normalment vols una configuració diferent per desenvolupament i producció. Utilitza variables d'entorn per fer-ho:
// config/cors.php
'allowed_origins' => explode(',', env('CORS_ALLOWED_ORIGINS', '')),# .env (desenvolupament)
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173
# .env (producció)
CORS_ALLOWED_ORIGINS=https://frontend.laravelandorra.com,https://app.laravelandorra.comMètodes i capçaleres#
// Permetre només els mètodes necessaris
'allowed_methods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
// Permetre totes les capçaleres (típic per a APIs)
'allowed_headers' => ['*'],
// O limitar a capçaleres específiques
'allowed_headers' => [
'Content-Type',
'Authorization',
'Accept',
'X-Requested-With',
],
// Exposar capçaleres personalitzades al client
'exposed_headers' => [
'X-Total-Count',
'X-Page-Count',
'X-Per-Page',
],Les exposed_headers són capçaleres de la resposta que el JavaScript del client pot llegir. Per defecte, el navegador només exposa unes poques capçaleres estàndard (Cache-Control, Content-Language, Content-Type, etc.). Si la teva API retorna capçaleres personalitzades (com comptadors de paginació), has d'exposar-les explícitament.
CORS amb credencials#
Quan necessites enviar cookies, capçaleres d'autenticació o certificats TLS amb les peticions cross-origin, has d'habilitar supports_credentials:
// config/cors.php
'supports_credentials' => true,
'allowed_origins' => ['https://frontend.laravelandorra.com'],Quan supports_credentials és true, no pots usar * com a origen permès. El navegador rebutja aquesta combinació per raons de seguretat. Has d'especificar els orígens exactes.
Al costat del client, les peticions han d'indicar explícitament que inclouen credencials:
// Amb Axios
axios.defaults.withCredentials = true;
// O per petició
axios.get('https://api.example.com/user', {
withCredentials: true,
});
// Amb Fetch API
fetch('https://api.example.com/user', {
credentials: 'include',
});CORS amb Sanctum (SPA authentication)#
Quan utilitzes Laravel Sanctum per autenticar una SPA, la configuració CORS i credencials és essencial:
// config/cors.php
return [
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:3000')],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => true, // Necessari per cookies de sessió
];// config/sanctum.php
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS',
'localhost,localhost:3000,127.0.0.1'
)),# .env
FRONTEND_URL=http://localhost:3000
SANCTUM_STATEFUL_DOMAINS=localhost:3000
SESSION_DOMAIN=localhostAquesta configuració permet que la SPA a localhost:3000 enviï cookies de sessió a l'API a localhost:8000. Sanctum utilitza cookies de sessió (no tokens) per a SPAs, cosa que requereix que CORS permeti credencials i que els dominis estiguin configurats com a "stateful".
Depurar errors de CORS#
Els errors de CORS són un dels problemes més frustrantes durant el desenvolupament. El navegador mostra un missatge genèric que no indica la causa exacta. Aquí tens els errors més comuns i les seves solucions:
"No 'Access-Control-Allow-Origin' header is present"#
El servidor no retorna la capçalera CORS. Causes possibles:
// Verificar que la ruta està inclosa als paths de CORS
'paths' => ['api/*'], // La ruta ha de coincidir amb el patró
// Si la ruta és /api/articles, coincideix amb api/* ✓
// Si la ruta és /sanctum/csrf-cookie, NO coincideix ✗
// Solució: afegir 'sanctum/csrf-cookie' als paths
'paths' => ['api/*', 'sanctum/csrf-cookie'],"The value of the 'Access-Control-Allow-Origin' header must not be the wildcard '*'"#
Estàs usant * amb credencials habilitades:
// Malament
'allowed_origins' => ['*'],
'supports_credentials' => true,
// Bé: especificar l'origen exacte
'allowed_origins' => ['https://frontend.example.com'],
'supports_credentials' => true,"Request header field X-Custom is not allowed"#
El client envia una capçalera que no està a la llista permesa:
// Solució: afegir la capçalera o usar el wildcard
'allowed_headers' => ['*'],
// O específicament
'allowed_headers' => ['Content-Type', 'Authorization', 'X-Custom'],La petició OPTIONS retorna 405 Method Not Allowed#
El servidor no gestiona la petició preflight. Laravel ho hauria de fer automàticament, però si tens un proxy o un servidor web que intercepta les peticions OPTIONS, cal configurar-lo perquè les deixi passar:
# Nginx: assegurar que OPTIONS arriba a Laravel
location /api {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Max-Age' 3600;
return 204;
}
try_files $uri $uri/ /index.php?$query_string;
}Verificar la configuració CORS#
Una manera ràpida de verificar que la configuració CORS és correcta és fer una petició preflight manualment amb curl:
# Simular una petició preflight
curl -X OPTIONS https://api.example.com/api/articles \
-H "Origin: https://frontend.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type, Authorization" \
-v
# La resposta hauria d'incloure:
# Access-Control-Allow-Origin: https://frontend.example.com
# Access-Control-Allow-Methods: GET, POST, PUT, DELETE
# Access-Control-Allow-Headers: Content-Type, AuthorizationOptimització del preflight#
Cada petició preflight afegeix una petició OPTIONS addicional, cosa que duplica el nombre de peticions. Per millorar el rendiment, configura max_age perquè el navegador guardi en cache les respostes preflight:
// config/cors.php
'max_age' => 86400, // 24 hores en segonsAmb max_age a 86400, el navegador fa la petició preflight un cop i guarda el resultat en cache durant 24 hores. Durant aquest temps, les peticions al mateix endpoint no necessiten preflight addicional. El valor màxim varia segons el navegador (Chrome limita a 7200 segons / 2 hores).
CORS en diferents escenaris#
API pública (sense autenticació)#
// config/cors.php
return [
'paths' => ['api/*'],
'allowed_methods' => ['GET'],
'allowed_origins' => ['*'],
'allowed_headers' => ['Content-Type', 'Accept'],
'max_age' => 86400,
'supports_credentials' => false,
];API privada (amb token Bearer)#
// config/cors.php
return [
'paths' => ['api/*'],
'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE'],
'allowed_origins' => [env('FRONTEND_URL')],
'allowed_headers' => ['Content-Type', 'Authorization', 'Accept'],
'max_age' => 3600,
'supports_credentials' => false,
];SPA amb Sanctum (cookies de sessió)#
// config/cors.php
return [
'paths' => ['api/*', 'sanctum/csrf-cookie', 'login', 'logout'],
'allowed_methods' => ['*'],
'allowed_origins' => [env('FRONTEND_URL')],
'allowed_headers' => ['*'],
'max_age' => 0,
'supports_credentials' => true,
];Múltiples frontends#
// config/cors.php
return [
'paths' => ['api/*'],
'allowed_methods' => ['*'],
'allowed_origins' => explode(',', env('CORS_ALLOWED_ORIGINS', '')),
'allowed_headers' => ['*'],
'max_age' => 3600,
'supports_credentials' => true,
];CORS_ALLOWED_ORIGINS=https://web.example.com,https://mobile.example.com,https://admin.example.comAquesta configuració permet gestionar els orígens des del fitxer .env sense necessitat de tocar el codi. Afegir un nou frontend és tan senzill com afegir-lo a la variable d'entorn i redesplegar.