Управление CORS через PHP: полный разбор заголовка Access-Control-Allow-Origin

Раздел: Веб-разработка -> Настройка HTTP заголовков

Настройка 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');
?>
Результат: В ответе будет только один заголовок с нужным значением.

Настройка заголовка Access-Control-Allow-Origin в PHP - comments

En
Php allow origin (php)