Kas gi tas CORS ir kodėl jis mums gadina gyvenimą?
Turbūt kiekvienas web developeris bent kartą gyvenime yra susidūręs su ta keista klaida naršyklės konsolėje: „Access to XMLHttpRequest has been blocked by CORS policy”. Pirmą kartą tai pamatęs, tikriausiai pagalvoji – kas čia per velnias? Veikė gi viskas puikiai lokaliai, o dabar staiga serveris atsisako bendrauti.
CORS (Cross-Origin Resource Sharing) – tai saugumo mechanizmas, kurį įgyvendina naršyklės. Jo esmė paprasta: jis neleidžia vienoje domenų esančiai svetainei tiesiog taip sau daryti užklausų į kitus domenus. Pavyzdžiui, jei tavo JavaScript kodas veikia ant example.com, jis negali tiesiog taip sau siųsti užklausų į api.kitasdomenas.com – nebent tas kitas domenas aiškiai pasakytų: „Taip, aš leidžiu”.
Kodėl taip? Na, įsivaizduok situaciją: tu prisijungęs prie savo banko svetainės, o kitame naršyklės skirtuke atsidari kažkokią abejotiną svetainę. Be CORS apsaugos, ta abejotina svetainė galėtų siųsti užklausas į tavo banko API naudodamasi tavo sesijos slapukais. Skamba baisu? Būtent todėl CORS ir egzistuoja.
Kaip CORS veikia po gaubtu
Kai tavo JavaScript kodas bando padaryti cross-origin užklausą, naršyklė pirmiausia pasižiūri – ar tai „paprasta” užklausa. Paprastos užklausos – tai GET arba POST su standartiniais headers, be jokių fancy dalykų. Jei užklausa paprasta, naršyklė ją išsiunčia ir tada tikrina serverio atsakymą. Serveris turi grąžinti specialų headerį `Access-Control-Allow-Origin`, kuris nurodo, kas gali naudoti šį resursą.
Bet jei užklausa sudėtingesnė (pavyzdžiui, PUT, DELETE, arba turi custom headers), naršyklė pirmiausia išsiunčia taip vadinamą „preflight” užklausą. Tai OPTIONS tipo užklausa, kuri klausia serverio: „Ei, ar man leidžiama čia daryti tokius dalykus?” Serveris turi atsakyti, kokius metodus leidžia, kokius headers priima ir panašiai. Tik gavusi leidimą, naršyklė išsiunčia tikrąją užklausą.
Štai kaip atrodo tipinis preflight užklausos ir atsakymo dialogas:
„`
OPTIONS /api/users HTTP/1.1
Origin: https://example.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Authorization
„`
Serveris atsako:
„`
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST, DELETE
Access-Control-Allow-Headers: Authorization
Access-Control-Max-Age: 86400
„`
Dažniausios CORS klaidos ir ką jos reiškia
Pirmoji ir dažniausia klaida: „No ‘Access-Control-Allow-Origin’ header is present”. Tai reiškia, kad serveris paprasčiausiai negrąžino reikiamo headerio. Sprendimas paprastas – reikia sukonfigūruoti serverį, kad jis tą headerį grąžintų.
Antroji populiari: „The ‘Access-Control-Allow-Origin’ header contains multiple values”. Čia problema ta, kad kažkas (dažniausiai per klaidą) pridėjo tą headerį kelis kartus. Gali būti, kad jį prideda ir tavo aplikacijos kodas, ir web serveris (Apache, Nginx). Reikia pasitikrinti abu ir palikti tik vieną vietą, kur tas headeris pridedamas.
Trečioji: „Response to preflight request doesn’t pass access control check”. Tai reiškia, kad tavo serveris netinkamai atsako į OPTIONS užklausas. Galbūt visai jų neapdoroja, arba grąžina netinkamus headers.
Dar viena įdomi: „Credentials flag is ‘true’, but ‘Access-Control-Allow-Origin’ is ‘*'”. Kai siunti užklausas su credentials (slapukais), negali naudoti wildcard (*) – turi nurodyti konkretų domeną.
Backend sprendimai: Express.js pavyzdys
Jei naudoji Node.js su Express, paprasčiausias būdas – naudoti `cors` paketą:
„`javascript
const express = require(‘express’);
const cors = require(‘cors’);
const app = express();
// Paprasčiausias variantas – leidžia visiems
app.use(cors());
// Arba su konfigūracija
app.use(cors({
origin: ‘https://tavo-frontend.com’,
credentials: true,
optionsSuccessStatus: 200
}));
„`
Bet jei nori daugiau kontrolės arba naudoji kitus frameworks, gali pridėti headers rankiniu būdu:
„`javascript
app.use((req, res, next) => {
res.header(‘Access-Control-Allow-Origin’, ‘https://tavo-frontend.com’);
res.header(‘Access-Control-Allow-Methods’, ‘GET, POST, PUT, DELETE, OPTIONS’);
res.header(‘Access-Control-Allow-Headers’, ‘Content-Type, Authorization’);
res.header(‘Access-Control-Allow-Credentials’, ‘true’);
// Svarbu: OPTIONS užklausoms grąžink 200
if (req.method === ‘OPTIONS’) {
return res.sendStatus(200);
}
next();
});
„`
Viena svarbi detalė – `Access-Control-Max-Age` headeris. Jis nurodo, kiek laiko (sekundėmis) naršyklė gali cache’inti preflight atsakymą. Jei nenurodai, naršyklė siųs preflight užklausą kiekvieną kartą, o tai lėtina tavo aplikaciją.
Nginx ir Apache konfigūracijos
Dažnai CORS headers patogiau pridėti web serverio lygyje, ypač jei turi kelis backend’us arba mikroservisų architektūrą.
Nginx konfigūracija:
„`nginx
location /api {
if ($request_method = ‘OPTIONS’) {
add_header ‘Access-Control-Allow-Origin’ ‘$http_origin’ always;
add_header ‘Access-Control-Allow-Methods’ ‘GET, POST, PUT, DELETE, OPTIONS’ always;
add_header ‘Access-Control-Allow-Headers’ ‘Authorization, Content-Type’ always;
add_header ‘Access-Control-Max-Age’ 1728000;
add_header ‘Content-Type’ ‘text/plain charset=UTF-8’;
add_header ‘Content-Length’ 0;
return 204;
}
add_header ‘Access-Control-Allow-Origin’ ‘$http_origin’ always;
add_header ‘Access-Control-Allow-Credentials’ ‘true’ always;
add_header ‘Access-Control-Allow-Methods’ ‘GET, POST, PUT, DELETE, OPTIONS’ always;
add_header ‘Access-Control-Allow-Headers’ ‘Authorization, Content-Type’ always;
proxy_pass http://backend;
}
„`
Apache su .htaccess:
„`apache
SetEnvIf Origin „^http(s)?://(.+\.)?(tavo-domenas\.com|localhost:3000)$” ORIGIN_DOMAIN=$0
Header set Access-Control-Allow-Origin „%{ORIGIN_DOMAIN}e” env=ORIGIN_DOMAIN
Header set Access-Control-Allow-Credentials „true”
Header set Access-Control-Allow-Methods „GET, POST, PUT, DELETE, OPTIONS”
Header set Access-Control-Allow-Headers „Authorization, Content-Type”
RewriteEngine On
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=204,L]
„`
Frontend pusės triukai ir sprendimo būdai
Kartais backend’as nėra tavo rankose – dirbi su trečiųjų šalių API, kuris neturi CORS palaikymo. Ką daryti?
Pirmasis variantas – proxy. Vietoj tiesioginio kreipimosi į problemišką API, kreipkis į savo serverį, o jis jau kreipiasi į tą API. Serveris-serveriui komunikacija neturi CORS apribojimų, nes CORS – tai tik naršyklės dalykas.
Su Create React App tai paprasta – `package.json` faile pridedi:
„`json
{
„proxy”: „https://problemiška-api.com”
}
„`
Arba sukuri paprastą proxy serverį:
„`javascript
const express = require(‘express’);
const axios = require(‘axios’);
const app = express();
app.get(‘/api/*’, async (req, res) => {
try {
const response = await axios.get(`https://problemiška-api.com${req.path}`);
res.json(response.data);
} catch (error) {
res.status(500).json({ error: ‘Kažkas nutiko’ });
}
});
app.listen(3001);
„`
Antrasis variantas – CORS proxy servisai kaip cors-anywhere. Bet atsargiai su jais production’e – jie gali būti lėti, nepatikimi, ir kelia saugumo klausimų.
Trečiasis – JSONP, bet tai sena technologija ir veikia tik su GET užklausomis. Šiais laikais geriau jos nenaudoti.
Saugumo aspektai ir best practices
Didžiausia klaida, kurią matau – `Access-Control-Allow-Origin: *` production’e. Taip, tai išsprendžia CORS problemą, bet atidaro duris saugumo problemoms. Jei tavo API reikalauja autentifikacijos, wildcard naudoti negalima – turi nurodyti konkretų domeną.
Jei turi keletą leidžiamų domenų, nedaryk taip:
„`javascript
// BLOGAI
res.header(‘Access-Control-Allow-Origin’, ‘https://domenas1.com, https://domenas2.com’);
„`
Vietoj to, tikrink Origin headerį ir grąžink tik tą, kuris atitinka:
„`javascript
// GERAI
const allowedOrigins = [
‘https://domenas1.com’,
‘https://domenas2.com’,
‘http://localhost:3000’
];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.header(‘Access-Control-Allow-Origin’, origin);
}
„`
Dar vienas svarbus dalykas – nebūtinai leisk visus metodus. Jei tavo API naudoja tik GET ir POST, nenurodyk DELETE ar PUT `Access-Control-Allow-Methods`.
Credentials – tai atskirą istoriją. Jei naudoji slapukus autentifikacijai, frontend’e turi nurodyti:
„`javascript
fetch(‘https://api.example.com/data’, {
credentials: ‘include’
});
// Arba su axios
axios.get(‘https://api.example.com/data’, {
withCredentials: true
});
„`
O backend’e:
„`javascript
res.header(‘Access-Control-Allow-Credentials’, ‘true’);
„`
Debugging ir troubleshooting patarimai
Kai susiduri su CORS problema, pirmiausia atsidaryk naršyklės Developer Tools Network tab’ą. Pažiūrėk į užklausas – ar matai OPTIONS užklausą prieš tikrąją? Jei taip, pažiūrėk į jos atsakymą. Kokie headers grąžinami?
Naudok `curl` testavimui iš komandinės eilutės:
„`bash
curl -H „Origin: https://tavo-frontend.com” \
-H „Access-Control-Request-Method: POST” \
-H „Access-Control-Request-Headers: Content-Type” \
-X OPTIONS \
–verbose \
https://tavo-api.com/endpoint
„`
Tai parodys tiksliai, ką serveris grąžina, be naršyklės „pagalbos”.
Dar vienas triukas – laikinai išjunk CORS naršyklėje testavimui. Chrome gali paleisti su:
„`bash
chrome –disable-web-security –user-data-dir=”/tmp/chrome_dev”
„`
Bet NIEKADA nedaryk šito su savo pagrindine naršykle ir NIEKADA nenaudok kaip sprendimo. Tai tik debugging įrankis.
Jei naudoji Postman ar panašius įrankius – atmink, kad jie neturi CORS apribojimų. Tai, kad užklausa veikia Postman’e, nereiškia, kad veiks naršyklėje.
Kai CORS tampa per daug sudėtingas
Kartais CORS konfigūracija tampa tokia sudėtinga, kad pradedi svarstyti alternatyvas. Viena iš jų – GraphQL su Apollo. Apollo Client turi įmontuotą CORS palaikymą ir dažnai lengviau konfigūruojasi.
Kita alternatyva – serverless funkcijos. Jei naudoji AWS Lambda, Netlify Functions ar Vercel, jos automatiškai tvarko CORS už tave (bent jau pagrindinius scenarijus).
Mikroservisų architektūroje gali naudoti API Gateway, kuris centralizuotai tvarko CORS visai sistemai. AWS API Gateway, Kong, ar Traefik turi įmontuotą CORS palaikymą.
WebSockets – tai visai kita istorija. Jie naudoja kitokį handshake protokolą, bet irgi turi origin checking. Socket.io biblioteka turi savo CORS konfigūraciją:
„`javascript
const io = require(‘socket.io’)(server, {
cors: {
origin: „https://tavo-frontend.com”,
methods: [„GET”, „POST”],
credentials: true
}
});
„`
Ir pagaliau – jei dirbi su legacy sistema, kuri tiesiog negali būti modifikuota pridėti CORS headers, vienintelis kelias – proxy. Sukurk tarpinį serverį, kuris prideda reikalingus headers ir perduoda užklausas toliau.
Ką daryti, kai viskas atrodo teisingai, bet vis tiek neveikia
Yra keletas subtilių dalykų, kurie gali sugadinti gyvenimą. Pirma – cache. Naršyklės agresyviai cache’ina preflight atsakymus. Jei keitei konfigūraciją, bet vis tiek matai seną klaidą – išvalyk cache arba naudok incognito režimą.
Antra – redirect’ai. Jei tavo serveris daro redirect (301, 302), CORS headers turi būti ir redirect atsakyme, ir galutiniame atsakyme. Dažnai žmonės pamiršta pridėti headers redirect’uose.
Trečia – trailing slash. `/api/users` ir `/api/users/` – tai skirtingi endpoint’ai kai kuriems serveriams. Jei vienas daro redirect į kitą, vėl grįžtam prie antro punkto.
Ketvirta – mixed content. Jei tavo frontend’as veikia per HTTPS, o backend per HTTP, naršyklė blokuos užklausas net su teisingais CORS headers. Turi naudoti HTTPS abiem pusėm arba abu HTTP (bet tik development’e).
Penkta – browser extensions. Kai kurie ad blockers ar privacy extensions gali blokuoti užklausas arba modifikuoti headers. Išbandyk su išjungtais extensions.
Šešta – serverio konfigūracija. Jei naudoji ir Nginx, ir Express CORS middleware, gali gauti dubliuotus headers. Pasirink vieną vietą, kur pridedi CORS headers.
Septinta – environment. Lokaliai veikia, production’e ne? Patikrink, ar environment variables teisingai nustatytos. Dažnai `ALLOWED_ORIGINS` ar panašūs kintamieji skiriasi tarp aplinkų.
Galiausiai, jei viskas kitas žlunga – logink viską. Pridėk logging middleware, kuris rodo kiekvieną užklausą, jos headers, origin, metodą. Dažnai problema tampa akivaizdi, kai matai tiksliai, kas vyksta.
CORS gali atrodyti kaip bereikalingas galvos skausmas, bet jis egzistuoja dėl svarbios priežasties – saugumo. Supratęs, kaip jis veikia, gali jį teisingai sukonfigūruoti ir užmiršti. Svarbiausia – neskubėti su `Access-Control-Allow-Origin: *` ir skirti laiko teisingai konfigūracijai. Taip, gali užtrukti valandą ar dvi pirmą kartą, bet vėliau sutaupysi daug nervų ir laiko.
