Пошук уроків, статей та іншого контенту
Налаштуєте CORS для контрольованого обміну даними між джерелами та зрозумієте роль preflight-запитів.
CORS (Cross-Origin Resource Sharing) — механізм браузера, який визначає, чи може вебсторінка з одного джерела отримувати відповіді від сервера іншого джерела.
Наприклад, сторінка:
https://app.example.comнадсилає запит до:
https://api.example.comЦе різні джерела, оскільки відрізняються хости. Сервер API має явно дозволити такий доступ за допомогою HTTP-заголовків CORS.
CORS не є окремим протоколом або бібліотекою. Це набір правил браузера та заголовків HTTP.
Джерело (origin) складається з трьох частин:
протоколу;
хоста;
порту.
Наприклад:
https://example.com:443Два URL мають однакове джерело, якщо всі три частини збігаються.
| URL | Те саме джерело? | Причина | |---|---:|---| | https://example.com/users | так | шлях не враховується | | http://example.com | ні | інший протокол | | https://api.example.com | ні | інший хост | | https://example.com:8443 | ні | інший порт |
CORS стає важливим лише для міжджерельних запитів.
За замовчуванням браузер застосовує політику same-origin policy. Вона не дозволяє JavaScript вільно читати відповіді від іншого джерела.
Наприклад:
fetch("https://api.example.com/users")
.then((response) => response.json())
.then((users) => {
console.log(users);
});Якщо https://api.example.com не поверне відповідний CORS-заголовок, браузер заблокує доступ JavaScript до відповіді.
Важливо: сервер все одно може отримати запит. CORS переважно контролює, чи зможе браузер передати відповідь коду сторінки.
Access-Control-Allow-OriginНайважливіший заголовок CORS — Access-Control-Allow-Origin.
Сервер може дозволити конкретне джерело:
Access-Control-Allow-Origin: https://app.example.comАбо всі джерела:
Access-Control-Allow-Origin: *Використання * підходить лише для публічних ресурсів, які не потребують облікових даних користувача.
Не слід бездумно копіювати значення заголовка Origin у відповідь:
response.setHeader("Access-Control-Allow-Origin", request.headers.origin);Так сервер фактично дозволить будь-яке джерело. Натомість потрібно перевіряти джерело за списком дозволених значень.
OriginБраузер додає до міжджерельного запиту заголовок:
Origin: https://app.example.comСервер може порівняти це значення зі списком дозволених джерел.
Origin не містить шлях:
https://app.example.comТому https://app.example.com/dashboard і https://app.example.com/settings мають однакове джерело.
Браузер може надіслати міжджерельний запит без попередньої перевірки, якщо він відповідає умовам простого запиту.
Зазвичай це:
метод GET, HEAD або POST;
відсутність нестандартних заголовків;
для Content-Type використовуються лише:
application/x-www-form-urlencoded;
multipart/form-data;
text/plain.
Наприклад:
fetch("https://api.example.com/status")
.then((response) => response.json())
.then((data) => {
console.log(data);
});Навіть для простого запиту сервер має повернути:
Access-Control-Allow-Origin: https://app.example.comЯкщо цього заголовка немає, браузер не дозволить JavaScript прочитати відповідь.
Якщо запит не є простим, браузер спочатку надсилає спеціальний запит OPTIONS. Він називається preflight-запитом.
Наприклад, цей запит зазвичай потребує preflight:
fetch("https://api.example.com/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Request-Id": "request-123"
},
body: JSON.stringify({
name: "Olena"
})
});Причини:
використовується Content-Type: application/json;
додається власний заголовок X-Request-Id.
Перед основним POST браузер може надіслати:
OPTIONS /users HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, x-request-idСервер повинен підтвердити, що такий запит дозволено:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, X-Request-IdЯкщо відповідь на preflight не задовольняє браузер, основний POST не буде надіслано.
Нижче наведено мінімальний сервер на вбудованому модулі Node.js. Він:
дозволяє лише два джерела;
обробляє preflight-запити;
дозволяє методи GET і POST;
дозволяє заголовки Content-Type і X-Request-Id;
повертає JSON-відповідь.
const http = require("node:http");
const allowedOrigins = new Set([
"http://localhost:5500",
"https://app.example.com"
]);
const server = http.createServer((request, response) => {
const origin = request.headers.origin;
if (origin && allowedOrigins.has(origin)) {
response.setHeader("Access-Control-Allow-Origin", origin);
response.setHeader("Vary", "Origin");
}
if (request.method === "OPTIONS") {
const requestedMethod = request.headers["access-control-request-method"];
const requestedHeaders =
request.headers["access-control-request-headers"];
const originIsAllowed = origin && allowedOrigins.has(origin);
const methodIsAllowed = ["GET", "POST"].includes(requestedMethod);
const requestedHeaderList = requestedHeaders
? requestedHeaders
.split(",")
.map((header) => header.trim().toLowerCase())
: [];
const allowedHeaders = new Set(["content-type", "x-request-id"]);
const headersAreAllowed = requestedHeaderList.every((header) =>
allowedHeaders.has(header)
);
if (originIsAllowed && methodIsAllowed && headersAreAllowed) {
response.statusCode = 204;
response.setHeader(
"Access-Control-Allow-Methods",
"GET, POST, OPTIONS"
);
response.setHeader(
"Access-Control-Allow-Headers",
"Content-Type, X-Request-Id"
);
response.setHeader("Access-Control-Max-Age", "600");
response.end();
return;
}
response.statusCode = 403;
response.end("CORS preflight rejected");
return;
}
if (request.method === "GET" && request.url === "/api/status") {
response.setHeader("Content-Type", "application/json");
response.end(
JSON.stringify({
ok: true,
message: "API працює"
})
);
return;
}
if (request.method === "POST" && request.url === "/api/users") {
let body = "";
request.setEncoding("utf8");
request.on("data", (chunk) => {
body += chunk;
});
request.on("end", () => {
let user;
try {
user = JSON.parse(body);
} catch {
response.statusCode = 400;
response.setHeader("Content-Type", "application/json");
response.end(JSON.stringify({ error: "Некоректний JSON" }));
return;
}
response.statusCode = 201;
response.setHeader("Content-Type", "application/json");
response.end(
JSON.stringify({
id: 1,
name: user.name
})
);
});
return;
}
response.statusCode = 404;
response.end("Not found");
});
server.listen(3000, () => {
console.log("API запущено на http://localhost:3000");
});Якщо сторінка працює з http://localhost:5500, запит до http://localhost:3000 є міжджерельним: порти відрізняються.
Запустіть сервер:
node server.jsСторінка, яка виконує запит:
<!doctype html>
<html lang="uk">
<head>
<meta charset="utf-8">
<title>CORS demo</title>
</head>
<body>
<button id="load-status">Отримати статус</button>
<button id="create-user">Створити користувача</button>
<pre id="output"></pre>
<script>
const output = document.querySelector("#output");
document.querySelector("#load-status").addEventListener("click", async () => {
try {
const response = await fetch("http://localhost:3000/api/status");
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
output.textContent = JSON.stringify(data, null, 2);
} catch (error) {
output.textContent = `Помилка: ${error.message}`;
}
});
document.querySelector("#create-user").addEventListener("click", async () => {
try {
const response = await fetch("http://localhost:3000/api/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Request-Id": "demo-1"
},
body: JSON.stringify({
name: "Олена"
})
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
output.textContent = JSON.stringify(data, null, 2);
} catch (error) {
output.textContent = `Помилка: ${error.message}`;
}
});
</script>
</body>
</html>Перший запит є простішим. Другий спочатку викликає OPTIONS, оскільки містить JSON і власний заголовок.
Якщо сервер повертає різний Access-Control-Allow-Origin залежно від значення Origin, потрібно додати:
Vary: OriginЦе повідомляє кешам, що відповідь для різних джерел може відрізнятися.
Без Vary: Origin кеш може помилково віддати відповідь, сформовану для одного джерела, іншому джерелу.
Для preflight браузер може кешувати дозвіл. Тривалість такого кешування задається заголовком:
Access-Control-Max-Age: 600Значення вказується в секундах.
За замовчуванням fetch не надсилає міжджерельні cookies.
Щоб дозволити їх надсилання, клієнт має вказати:
fetch("https://api.example.com/profile", {
credentials: "include"
});Сервер у такому разі повинен повернути:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: trueКомбінація нижче є некоректною:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: trueЯкщо використовуються credentials, сервер повинен указати конкретне джерело, а не *.
Cookie також має бути придатною для міжджерельного використання. Зокрема, у відповідних сценаріях можуть бути потрібні атрибути:
Set-Cookie: session=abc123; Secure; HttpOnly; SameSite=NoneHttpOnly забороняє читати cookie через JavaScript, але не заважає браузеру надсилати її автоматично.
CORS не замінює автентифікацію та авторизацію.
Заголовок:
Access-Control-Allow-Origin: https://app.example.comлише дозволяє сторінці прочитати відповідь. Він не доводить, що користувач має право виконати операцію.
Сервер все одно повинен:
перевіряти токен або сесію;
перевіряти права користувача;
валідувати вхідні дані;
обмежувати доступ до ресурсів;
захищатися від CSRF, якщо автентифікація використовує cookies.
Особливо важливо розрізняти:
CORS — контроль доступу браузера до відповіді;
CSRF — захист від небажаних дій, виконаних браузером користувача;
авторизацію — перевірку прав на виконання операції.
Навіть якщо CORS налаштовано правильно, публічний API може бути викликаний через curl, серверний код або інший HTTP-клієнт. CORS не є універсальним мережевим firewall.
Браузер дозволяє JavaScript читати лише частину заголовків відповіді. Якщо API повертає власний заголовок, його потрібно явно відкрити:
Access-Control-Expose-Headers: X-Total-Count, X-Request-IdПісля цього клієнт зможе виконати:
const response = await fetch("https://api.example.com/users");
console.log(response.headers.get("X-Total-Count"));Без Access-Control-Expose-Headers сервер може фізично надіслати цей заголовок, але JavaScript не отримає до нього доступу.
CORS-заголовки мають додаватися не лише до успішних відповідей, а й до відповідей із помилками:
400 Bad Request;
401 Unauthorized;
403 Forbidden;
404 Not Found;
500 Internal Server Error.
Якщо CORS-заголовка немає на відповіді з помилкою, браузер часто покаже клієнту загальну CORS-помилку замість статусу та тіла відповіді API.
http://localhost:3000 і http://localhost:5500 — різні джерела. Порт потрібно враховувати під час налаштування allowlist.
* разом із credentialsЦе заборонена комбінація:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: trueПотрібно вказати конкретне джерело.
Якщо сервер відповідає на POST, але не обробляє OPTIONS, запит із preflight не працюватиме.
Якщо клієнт надсилає:
X-Request-Idсервер має дозволити його в:
Access-Control-Allow-HeadersНазви HTTP-заголовків не залежать від регістру, але їх потрібно коректно перелічити.
fetchКлієнт не може видати собі дозвіл на CORS таким кодом:
fetch(url, {
headers: {
"Access-Control-Allow-Origin": "*"
}
});Access-Control-Allow-Origin — це заголовок відповіді сервера, а не запиту клієнта.
mode: "no-cors" як виправленняfetch(url, {
mode: "no-cors"
});Це не дозволяє прочитати відповідь. У результаті буде непрозора (opaque) відповідь, з якої JavaScript не може отримати тіло, статус або більшість заголовків.
curl не застосовує політику same-origin так, як браузер. Успішний запит через curl не означає, що браузер дозволить прочитати відповідь.
Не варто дозволяти всі піддомени або довільно віддзеркалювати Origin, якщо API працює з приватними даними.
У DevTools відкрийте вкладку Network і перевірте:
чи є запит OPTIONS;
значення заголовка Origin;
Access-Control-Allow-Origin у відповіді;
дозволений метод у Access-Control-Allow-Methods;
дозволені заголовки у Access-Control-Allow-Headers;
чи не використовується credentials разом із *;
чи додаються CORS-заголовки до відповідей із помилками.
Повідомлення браузера на кшталт «blocked by CORS policy» описує наслідок. Справжня причина зазвичай визначається порівнянням заголовків запиту та відповіді.
Визначте точні джерела клієнтів.
Створіть allowlist дозволених джерел.
Додайте Access-Control-Allow-Origin лише для дозволених джерел.
Обробіть OPTIONS для preflight-запитів.
Перелічіть дозволені методи та заголовки.
Додавайте Vary: Origin, якщо значення джерела динамічне.
Окремо налаштуйте credentials, якщо використовуються cookies.
Перевірте CORS для успішних і помилкових відповідей.
Не сприймайте CORS як заміну автентифікації, авторизації або CSRF-захисту.
Джерело визначається протоколом, хостом і портом.
Політика same-origin обмежує читання міжджерельних відповідей JavaScript.
CORS налаштовується на сервері через HTTP-заголовки.
Простий запит може виконуватися без preflight, але відповідь усе одно повинна містити правильний CORS-заголовок.
Складний запит перед виконанням основної операції проходить OPTIONS preflight.
Access-Control-Allow-Origin визначає дозволене джерело.
Access-Control-Allow-Methods і Access-Control-Allow-Headers описують дозволені параметри preflight.
credentials: "include" потребує конкретного джерела та Access-Control-Allow-Credentials: true.
CORS не є механізмом автентифікації чи авторизації.
Надійне налаштування CORS передбачає allowlist, коректну обробку OPTIONS і мінімально необхідні дозволи.