Управление CORS через PHP: полный разбор заголовка Access-Control-Allow-Origin
Настройка Access-Control-Allow-Origin в PHP
Заголовок Access-Control-Allow-Origin является частью механизма CORS (Cross-Origin Resource Sharing) и определяет, какие внешние источники могут получать доступ к ресурсам сервера. В PHP этот заголовок устанавливается функцией header() до любого вывода данных.
Основное решение: установка заголовка через header()
Самый прямой способ - добавить строку header('Access-Control-Allow-Origin: *'); в начало скрипта. Звёздочка разрешает доступ с любого домена. Если требуется ограничить доступ одним конкретным доменом, указывают его явно: header('Access-Control-Allow-Origin: https://example.com');.
<?php
header('Access-Control-Allow-Origin: *');
// или
header('Access-Control-Allow-Origin: https://example.com');
?>
Php allow origin (настройка заголовка access-control-allow-origin в php)
Этот код должен быть выполнен до любого вывода (echo, print, пробелы перед <?php). Иначе заголовок не установится и PHP выдаст warning.
Варианты решений и сценариев
Как разрешить сразу несколько определённых источников?
Если нужно дать доступ нескольким доменам, нельзя использовать звёздочку вместе с конкретными адресами. Лучше проверять значение заголовка Origin запроса и динамически устанавливать ответный заголовок.
<?php
$allowed_origins = [
'https://site1.com',
'https://site2.org',
'http://localhost:3000'
];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowed_origins)) {
header("Access-Control-Allow-Origin: $origin");
} else {
header('Access-Control-Allow-Origin: none'); // или не отправлять заголовок
}
?>
Важно:
заголовок Origin присутствует только при кросс-доменных запросах. Если его нет, нужно решить, стоит ли вообще отправлять CORS-заголовок (часто имеет смысл для GET-запросов с одного домена).Как настроить CORS через .htaccess (Apache)?
Если PHP работает под управлением Apache, можно задать заголовок в конфигурации сервера, а не в коде. Это ускоряет обработку и не требует изменения PHP-файлов.
# .htaccess
Header set Access-Control-Allow-Origin "*"
# или для конкретного домена:
Header set Access-Control-Allow-Origin "https://example.com"
Для работы директивы Header должен быть включён модуль mod_headers.
Как обработать preflight-запрос (OPTIONS)?
Перед сложными запросами (с нестандартными заголовками, методами PUT/DELETE, с типом контента application/json) браузер отправляет предварительный запрос OPTIONS. В ответе сервер должен подтвердить разрешённые методы и заголовки.
<?php
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
// Разрешаем конкретный источник (или *)
header('Access-Control-Allow-Origin: https://example.com');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With');
// Максимальное время кэширования preflight-ответа (в секундах)
header('Access-Control-Max-Age: 86400');
// Отправляем пустой ответ
http_response_code(204);
exit;
}
?>
Если этого не сделать, браузер заблокирует основной запрос.
Как добавить поддержку учётных данных (cookies, авторизация)?
При использовании withCredentials на стороне клиента сервер должен явно разрешить это, вернув заголовок Access-Control-Allow-Credentials: true. При этом значение Access-Control-Allow-Origin не может быть звёздочкой - только конкретный домен.
<?php
header('Access-Control-Allow-Origin: https://example.com');
header('Access-Control-Allow-Credentials: true');
?>
Как разрешить доступ только определённым HTTP-методам?
Помимо origin можно ограничить методы, которые разрешены для внешних сайтов. Это делается заголовком Access-Control-Allow-Methods.
<?php
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
?>
Типичные ошибки и их решение
- Заголовок не отправляется: Убедитесь, что вызов header() происходит до любого вывода. Если в файле есть пробелы или HTML перед <?php, используйте ob_start() или вынесите вызов header() в начало отдельного скрипта.
- Браузер сообщает о множественных значениях Access-Control-Allow-Origin: Сервер не должен отправлять заголовок несколько раз с разными значениями. Лучше использовать динамическую проверку и один заголовок. Если в .htaccess и PHP одновременно заданы разные значения, возникнет конфликт.
- Preflight-запрос не проходит: Убедитесь, что для OPTIONS-запроса сервер возвращает корректные CORS-заголовки и статус 200 или 204. Также проверьте, что клиент не отправляет нестандартные заголовки, которые не перечислены в Access-Control-Allow-Headers.
- Звёздочка не работает с учётными данными: Запомните правило: если any origin (*) используется, нельзя использовать withCredentials. Либо уберите credentials, либо задайте конкретный origin.
- Кэширование CORS-ответа: Браузер может закэшировать ответ preflight-запроса. Используйте заголовок Access-Control-Max-Age для управления временем жизни кэша. Если нужно изменить политику, уменьшите значение или добавьте уникальность через vary.
Расширенные примеры кода с пояснениями
Пример 1: Динамический список разрешённых доменов с проверкой по регулярному выражению
Иногда требуется разрешить все поддомены определённого домена. Используем регулярное выражение для проверки origin.
<?php
function setCorsOrigin() {
$allowed_pattern = '/^https?:\/\/([a-z0-9\-]+\.)*example\.com$/i';
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (preg_match($allowed_pattern, $origin)) {
header("Access-Control-Allow-Origin: $origin");
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
} else {
// Если origin не подходит, не отправляем CORS-заголовок (браузер заблокирует запрос)
// Или можно выдать 403
http_response_code(403);
echo 'CORS policy does not allow this origin.';
exit;
}
}
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
setCorsOrigin();
http_response_code(204);
exit;
}
setCorsOrigin();
// Основной код API
?>
Результат: При запросе с https://sub.example.com заголовок будет установлен для этого конкретного поддомена. Если запрос придёт с https://evil.com, вернётся 403.
Пример 2: Использование нескольких допустимых методов и заголовков в Preflight
Покажем полную обработку сложного запроса с проверкой всех разрешённых параметров.
<?php
// Настройки
$allowed_origins = ['https://app.example.com', 'https://admin.example.com'];
$allowed_methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
$allowed_headers = ['Content-Type', 'Authorization', 'X-Requested-With', 'X-Custom-Header'];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
function sendCorsHeaders($origin, $methods, $headers) {
header("Access-Control-Allow-Origin: $origin");
header('Access-Control-Allow-Credentials: true');
header("Access-Control-Allow-Methods: " . implode(', ', $methods));
header("Access-Control-Allow-Headers: " . implode(', ', $headers));
header('Access-Control-Max-Age: 3600');
}
if (in_array($origin, $allowed_origins)) {
sendCorsHeaders($origin, $allowed_methods, $allowed_headers);
} else {
http_response_code(403);
echo 'Origin not allowed';
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
// Дальше идёт основная логика (например, REST API)
echo json_encode(['status' => 'OK']);
?>
Результат: Браузер получит все необходимые CORS-заголовки, preflight будет обработан корректно.
Пример 3: Установка CORS-заголовков для всех файлов в проекте через bootstrap
Чтобы не дублировать код в каждом скрипте, создайте единый файл cors.php и подключайте его в начале публичных скриптов.
// cors.php
<?php
$allowed_domains = ['https://frontend.com', 'http://localhost:8080'];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowed_domains)) {
header("Access-Control-Allow-Origin: $origin");
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Allow-Methods: GET, POST, PUT, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
} else {
// Можно ничего не делать, браузер сам заблокирует
}
?>
// index.php
<?php
require_once 'cors.php';
// ...
?>
Результат: Все запросы к index.php будут проходить CORS-проверку. Если origin совпадает, заголовки добавляются; preflight завершается раньше выполнения основного кода.
Пример 4: Использование заголовка Vary для корректного кэширования
Когда origin динамический, браузеры и прокси могут кэшировать ответы. Чтобы не произошла путаница, полезно добавить заголовок Vary: Origin.
<?php
$origin = $_SERVER['HTTP_ORIGIN'] ?? '*';
header("Access-Control-Allow-Origin: $origin");
header('Vary: Origin');
?>
Результат: Кэш будет учитывать значение заголовка Origin при сохранении ответа, что исключит выдачу ответа одному домену для другого.
Пример 5: Настройка CORS при помощи библиотеки (например, slim/cors)
В крупных проектах удобно использовать готовые пакеты. Пример для фреймворка Slim:
// composer require slim/cors
use Slim\App;
use Slim\Middleware\ErrorMiddleware;
use Slim\Psr7\Response;
use Psr\Http\Message\ServerRequestInterface;
$app = new App();
// Добавляем middleware для CORS
$app->add(function (ServerRequestInterface $request, $handler) {
$response = $handler->handle($request);
return $response
->withHeader('Access-Control-Allow-Origin', 'https://example.com')
->withHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
});
$app->options('/{routes:.*}', function (ServerRequestInterface $request, Response $response) {
return $response->withStatus(204);
});
$app->get('/hello', function (ServerRequestInterface $request, Response $response) {
$response->getBody()->write('Hello, CORS!');
return $response;
});
$app->run();
Результат: Все маршруты получат CORS-заголовки, preflight обрабатывается автоматически.
Пример 6: Обработка ошибки при дублировании заголовков от сервера и PHP
Если .htaccess уже задаёт заголовок, и PHP пытается переопределить его, может возникнуть конфликт. Решение - отключить установку в .htaccess для конкретных файлов или использовать header_remove() в PHP.
<?php
header_remove('Access-Control-Allow-Origin'); // удалить, если был установлен ранее
header('Access-Control-Allow-Origin: https://newdomain.com');
?>
Результат: В ответе будет только один заголовок с нужным значением.