Оглавление
- Введение
- Что такое CORS и зачем он нужен
- Как работает механизм CORS
- Работа с CORS в разработке и продакшн
- Реализация CORS в Spring Boot
- Реализация CORS в Go
- Заключение
Введение
Каждый веб-разработчик хотя бы раз сталкивался с сообщением об ошибке в консоли браузера, похожим на:
Access to fetch at 'https://api.example.com/data' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Эта ошибка означает, что браузер заблокировал HTTP-запрос к ресурсу на другом домене из соображений безопасности. Подобные CORS-ошибки часто раздражают в процессе разработки, но на самом деле механизм CORS чрезвычайно важен для безопасности веб-приложений. В этой статье мы подробно разберём, что такое CORS, почему он необходим, как правильно его настраивать на бэкенде (на примере Spring Boot и Go), а также рассмотрим типичные ситуации в разработке и продакшене.
Что такое CORS и зачем он нужен
CORS (Cross-Origin Resource Sharing) – это механизм, позволяющий веб-браузеру запрашивать ресурсы с другого домена (источника) при условии, что сервер явно разрешает такой доступ. Этот механизм расширяет базовое правило безопасности под названием Same-Origin Policy (политика одинакового источника). По умолчанию веб-страница может отправлять запросы только к тому же самому источнику, с которого она была загружена. Браузеры определяют «источник» по комбинации схемы (протокола), доменного имени и порта. Если хотя бы один из этих параметров отличается между страницей и запрашиваемым ресурсом, то они считаются разными источниками (cross-origin). Например, страница с адресом http://example.com
находится на другом origin относительно ресурса https://example.com
(отличается схема), или даже относительно http://example.com:8080
(отличается порт).
Политика одинакового источника была введена в браузеры как мера безопасности, чтобы вредоносный код на одной странице не мог получить доступ к данным с другого сайта. Без этого ограничения злоумышленник мог бы, к примеру, вложить на свой сайт скрипт, который автоматически отправит запрос от вашего имени к сайту банка (при этом браузер передаст ваши авторизационные куки) – и выполнить нежелательное действие (например, попытаться удалить ваш аккаунт). Именно для предотвращения таких атак браузеры по умолчанию и блокируют чтение ответов на cross-origin запросы. Однако современным приложениям часто требуется обращаться к сторонним API и ресурсам – например, загружать данные из внешнего REST API, получать шрифты из CDN или показывать погоду с сервиса погоды. Здесь и приходит на помощь CORS: это стандарт, позволяющий серверу сообщить браузеру, каким внешним источникам можно доверять и разрешать доступ к своим ресурсам.
Проще говоря, CORS служит «предохранительным клапаном». Он сохраняет преимущества безопасности Same-Origin Policy, но позволяет легально сделать исключение для определённых доверенных источников. Сервер, настроенный с поддержкой CORS, отправляет в ответ на запрос специальные заголовки, которые говорят браузеру: «Да, я разрешаю странице с домена X получить этот ресурс». Без этих заголовков браузер не выдаст странице доступ к ответу сервера и покажет ошибку. Таким образом, только сервер контролирует, какие домены могут обращаться к его ресурсам. Это важно для предотвращения утечек данных и межсайтовых атак, а также для интеграции вашего приложения с внешними сервисами безопасным способом.

Диаграмма 1: Пользователь открывает веб-приложение на сайте A. Приложение (в браузере) посылает запрос к API на сайте B. Без CORS браузер бы заблокировал этот запрос, но при правильно настроенном CORS сервер B вернёт ответ, и браузер передаст данные приложению.
Как работает механизм CORS
Рассмотрим пошагово, что происходит при cross-origin запросе и как срабатывает механизм CORS. Когда браузер загружает страницу, он знает её origin (например, http://mysite.com
). Если JavaScript на этой странице пытается выполнить запрос к другому домену (например, http://api.other.com
), браузер сначала сравнивает источники. Обнаружив, что домены или порты не совпадают, браузер помечает запрос как кросс-доменный и по умолчанию блокирует его отправку или получение ответа (в некоторых случаях запрос может быть отправлен, но ответ заблокирован).
Чтобы такой запрос был успешно выполнен, сервер api.other.com
должен явно разрешить запрос от origin mysite.com
. Для этого существуют специальные HTTP-заголовки, которые сервер возвращает в ответе. Основные заголовки, используемые для настройки CORS на сервере:
Access-Control-Allow-Origin
– указывает, с какого origin разрешён доступ. Например,Access-Control-Allow-Origin: https://mysite.com
означает, что только страницам с данного адреса разрешено получать ответ. Значение “*
” означает разрешение для любых доменов, но не рекомендуется в продакшне (об этом ниже).Access-Control-Allow-Methods
– перечень HTTP-методов, которые разрешено использовать при обращении к ресурсу. Например:GET, POST, PUT, DELETE
. Если метод запроса не перечислен, браузер не позволит его выполнить.Access-Control-Allow-Headers
– список нестандартных заголовков, которые можно использовать в запросе. Браузер потребует разрешения на любые заголовки кроме простых (как Content-Type, Accept и т.п.). Например, если клиентский код устанавливает свой заголовокX-Auth-Token
, сервер должен явно разрешить его через этот заголовок.Access-Control-Allow-Credentials
– флаг (true/false), разрешающий отправлять вместе с запросом учетные данные: куки, сессионные идентификаторы, токены авторизации в заголовках. По умолчанию браузер не будет включать креденшлы в cross-origin запросы, если сервер не указалAccess-Control-Allow-Credentials: true
. Важно отметить, что при использовании этого флага значениеAccess-Control-Allow-Origin
не может быть"*"
– нужно явно указать домен.
Помимо заголовков ответа сервера, браузер автоматически добавляет в каждый cross-origin запрос заголовок Origin
, где указывает домен источника страницы (например, Origin: http://mysite.com
). Сервер может использовать это значение, чтобы решить – отвечать ли запросу и какие заголовки CORS послать в ответ.
Если запрос является простым (например, обычный GET без специальных заголовков), фаза preflight опускается – браузер сразу отправит запрос, добавив заголовок Origin
, и ожидает от сервера непосредственного ответа с Access-Control-Allow-Origin
. Таким образом, последовательность укороченная, но суть остаётся той же: отсутствие нужного заголовка в ответе приводит к блокировке на стороне браузера.
Предварительный запрос (Preflight)
Для некоторых типов запросов браузер выполняет так называемый предварительный запрос перед основным. Предварительный запрос – это запрос метода OPTIONS, который браузер автоматически отправляет к ресурсу, чтобы заранее узнать, разрешает ли сервер к нему доступ. Предварительный запрос происходит в следующих случаях:
- Метод основного запроса не является простым (simple). К простым методам относятся GET, HEAD и POST (с некоторыми ограничениями). Запросы типа PUT, DELETE, PATCH и другие считаются небезопасными и требуют preflight.
- Запрос содержит нестандартные заголовки или имеет
Content-Type
, отличный отapplication/x-www-form-urlencoded
,multipart/form-data
илиtext/plain
. Например, если вы отправляете JSON (Content-Type: application/json
), браузер сделает preflight. - Также предварительные запросы всегда отправляются для методов типа DELETE, PUT и для всех POST, у которых контент-тайп не простой.
При preflight браузер отправляет OPTIONS-запрос на тот же URL, куда пойдёт основной запрос, включая заголовки:Origin: <домен страницы>
– откуда пришёл запрос;Access-Control-Request-Method: <метод>
– какой метод собираемся использовать;Access-Control-Request-Headers: <заголовки>
– какие нестандартные заголовки планируются в запросе (если есть).
Пример: страница http://mysite.com
хочет сделать AJAX-вызов POST /api/data
на http://api.other.com
с заголовком Content-Type: application/json
. Браузер сначала отправит:
- OPTIONS /api/data
Host: api.other.com
Origin: http://mysite.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type
Получив такой запрос, сервер если позволяет CORS для данного ресурса должен ответить без тела, но с набором разрешающих заголовков:
- HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://mysite.com
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Content-Type
Статус 204 (No Content) здесь означает, что нет тела ответа – это нормально для preflight. Браузер проанализирует этот ответ. Если Origin
не совпадёт или нужного метода/заголовка нет в разрешениях, браузер прекратит дальнейшие действия и отклонит запрос. Но если всё в порядке – браузер продолжит и отправит основной запрос (например, фактический POST с данными). При ответе на основной запрос сервер обычно тоже добавляет заголовок Access-Control-Allow-Origin: http://mysite.com
(и другие, при необходимости), чтобы браузер разрешил скрипту страницы прочитать ответ. Ниже представлена диаграмма последовательности, демонстрирующая этот процесс.
Важно, что браузер кеширует успешные результаты preflight-запросов на некоторый промежуток (можно настроить через заголовок Access-Control-Max-Age
). Это значит, что при повторных запросах к тому же ресурсу preflight может не выполняться каждый раз, пока не истечёт время кэша.
Теперь рассмотрим последовательность обмена сообщениями между браузером и сервером при выполнении кросс-доменного запроса. Ниже приведена UML-диаграмма последовательности (Sequence Diagram), описывающая сценарий с предварительным запросом (случай, когда нужен preflight, например метод POST с JSON):

Диаграмма 2: обмена сообщениями между браузером и сервером при выполнении кросс-доменного запроса.
На этой диаграмме показано, как браузер сначала посылает на сервер preflight-запрос (OPTIONS) с указанием своего origin и запрашиваемого метода. Сервер отвечает, разрешая нужные параметры (Origin, Method, Headers) и тем самым информируя браузер, что основной запрос можно отправлять. Затем браузер выполняет основной запрос (POST), и сервер возвращает данные, снова подтвердив Origin в заголовке. Браузер, видя заголовки разрешения, передаёт ответ JavaScript-коду страницы.
Обратите внимание и на ограничение: сервер не может отправить более одного значения в Access-Control-Allow-Origin
. Нельзя перечислить несколько доменов через запятую или пробел. Если необходимо разрешить доступ для нескольких сайтов, то серверная логика должна проверить заголовок Origin
входящего запроса и, если он есть в списке разрешённых, выставить ровно этот origin в заголовке ответа. В таких случаях также советуют добавлять заголовок Vary: Origin
, чтобы прокси и браузеры корректно кэшировали разные варианты ответа.
Работа с CORS в разработке и продакшн
Настройка CORS может отличаться в условиях разработки (на учебных проектах) и на боевой продакшн-среде. В процессе разработки фронтенд и бэкенд приложения часто запускаются на разных портах или даже доменах (например, React-приложение на localhost:3000
и API-сервер на localhost:8080
). Это разные origins, поэтому без настройки CORS браузер будет блокировать запросы. Быстрый способ решить проблему при локальной разработке – разрешить все источники. Например, можно временно выставлять Access-Control-Allow-Origin: *
(звёздочка означает «разрешить всем») для упрощения тестирования. Многие фреймворки даже позволяют одним махом включить «CORS для всех» в dev-режиме, чтобы не тратить время.
Однако в продакшне такой подход недопустим. Заголовок Access-Control-Allow-Origin: *
открывает доступ к вашему API с любого домена, что повышает риск злоумышленных запросов и атак типа CSRF. Лучше избегать разрешения для любого домена – вместо этого явно указывайте доверенные origin’ы, с которых ваше приложение должно быть доступно. Например, если ваш фронтенд хостится на https://myapp.com
, на бэкенде стоит разрешить только этот origin. Исключение можно делать для внутреннего тестирования (например, добавив также http://localhost:3000
для стадии разработки).
Также учтите, что если ваш API использует аутентификацию через куки или другие креденшлы, потребуется более строгая настройка: Access-Control-Allow-Credentials: true
и точное указание домена в Allow-Origin
(без звёздочки). Браузер в этом случае будет отправлять куки только если origin совпал и Credentials разрешены, и не позволит стороннему сайту украсть сессионный идентификатор.
В целом, в учебных проектах можно позволить себе более широкие настройки CORS (например, разрешить все методы и источники), чтобы сосредоточиться на разработке функционала. Но в продакшн-среде конфигурация CORS должна быть минимально достаточной: открывать доступ только нужным доменам, методам и заголовкам. Регулярно пересматривайте эти настройки и обновляйте их по мере изменений в вашем фронтенде или политиках безопасности.
Далее рассмотрим конкретно, как настроить CORS на бэкенде, используя два примера: приложение на Spring Boot (Java) и простой HTTP-сервер на Go.
Реализация CORS в Spring Boot
Spring Boot предоставляет встроенные средства для настройки CORS, благодаря Spring Framework. Есть несколько способов разрешить cross-origin запросы в Spring-бэкенде:
1. Аннотация @CrossOrigin
на контроллерах или методах. Это простой способ открыть конкретный контроллер или метод для запросов из других источников. Аннотация может задавать список разрешённых доменов через параметр origins
, а также настраивать другие аспекты (методы, заголовки, креденшлы и т.д.). Например, допустим у нас есть REST-контроллер с endpoint /api/hello
:
@RestController
@RequestMapping("/api")
public class TestController {
// Разрешаем вызовы этого метода с origin "http://localhost:3000"
@CrossOrigin(origins = "http://localhost:3000")
@GetMapping("/hello")
public String hello() {
return "Hello, CORS!";
}
}
В данном примере метод hello()
будет доступен с фронтенда, работающего на http://localhost:3000
. Браузер, обращаясь к /api/hello
на нашем сервере, получит в ответ заголовок Access-Control-Allow-Origin: http://localhost:3000
автоматически (это делает Spring, увидев аннотацию), и не заблокирует ответ. Аннотацию @CrossOrigin
можно также повесить на весь класс контроллера, тогда все его методы будут разрешены для указанных доменов. Если параметр origins
не указан, по умолчанию разрешаются все источники – в учебных целях это удобно, но для продакшна лучше всегда явно прописывать.
2. Глобальная конфигурация CORS. Если нужно настроить CORS сразу для всего приложения (например, открыть все API для определённого домена, либо задать сложные правила), удобнее воспользоваться глобальной конфигурацией Spring MVC. Это делается путем создания бина с реализацией WebMvcConfigurer
. Например, добавим класс конфигурации:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// Разрешаем CORS для всех путей /api/**
registry.addMapping("/api/**")
.allowedOrigins("https://myapp.com", "https://admin.myapp.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*");
}
}
Здесь мы программно указали, что для всех URL, начинающихся с /api/
, наш сервер будет отправлять необходимые CORS-заголовки. Разрешены два источника: основной сайт приложения и админ-панель (предположим, она на поддомене), а также четыре метода. Заголовки allowedHeaders("*")
означают, что мы не ограничиваем конкретные запрашиваемые заголовки – клиент может посылать любые (это равносильно тому, чтобы в ответе всегда дублировался список из запроса). При необходимости можно явно указать, какие нестандартные заголовки разрешены.
Spring Boot автоматически применит эту конфигурацию при старте приложения. В результате, нет нужды помечать каждую точку входа аннотациями – все URL, попадающие под шаблон, будут защищены CORS по заданным правилам. Учтите, что если у вас также используется Spring Security, может понадобиться дополнительная настройка, чтобы пропустить OPTIONS-запросы (preflight) без аутентификации, но это выходит за рамки нашего обзора.
Оба подхода можно комбинировать: например, глобально разрешить всё для /api/public/**
, а на отдельных контроллерах для чувствительных данных настроить свои ограничения. Spring обрабатывает CORS-конфигурации последовательно, применяя наиболее специфичную.
Реализация CORS в Go
В языке Go (Golang) низкоуровневая библиотека net/http
не имеет встроенной магии для CORS – разработчик должен сам позаботиться о необходимых заголовках. Однако реализовать это несложно. Рассмотрим минимальный пример HTTP-сервера, который обслуживает один маршрут /api/data
. Мы настроим его так, чтобы он позволял запросы с origin http://localhost:3000
(например, наше фронтенд-приложение на React) и поддерживал методы GET и POST с заголовком Content-Type.
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/api/data", func(w http.ResponseWriter, r *http.Request) {
// 1. Всегда устанавливаем разрешенный Origin (если нужен конкретный, подставляем его)
w.Header().Set("Access-Control-Allow-Origin", "http://localhost:3000")
// 2. Разрешаем необходимые методы и заголовки
w.Header().Set("Access-Control-Allow-Methods", "GET, POST")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
// 3. Если пришел preflight-запрос (OPTIONS), отвечаем сразу и выходим
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
// 4. Обработка основного запроса (например, GET)
if r.Method == http.MethodGet {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, `{ "message": "Hello, CORS from Go" }`)
} else {
w.WriteHeader(http.StatusMethodNotAllowed)
}
})
log.Println("Сервер запущен на :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Разберём, что тут происходит. Сначала, внутри обработчика, мы добавляем в заголовки ответа Access-Control-Allow-Origin: http://localhost:3000
. Тем самым этот маршрут открывается для AJAX-запросов с локального фронтенда. Затем Access-Control-Allow-Methods
и Access-Control-Allow-Headers
разрешают соответствующие методы (GET, POST) и заголовки (здесь только Content-Type
). После этого идёт проверка: если HTTP-метод запроса – OPTIONS, мы трактуем это как preflight. В ответ сразу посылаем статус 204 No Content (без тела) и выходим из обработчика, не выполняя основную логику. Браузер получит этот пустой ответ, увидит разрешающие заголовки (мы их уже поставили выше) – и, если всё ок, выполнит реальный запрос.
Для основного запроса (например, GET) наша функция возвращает простые данные в JSON. Обратите внимание, что для самого ответа мы тоже оставили в заголовках Access-Control-Allow-Origin
. Это важно: браузер проверяет заголовок каждого ответа (не только preflight). Если при ответе на GET этого заголовка не окажется, браузер снова заблокирует доступ к данным.
Приведённый способ – максимально простой и наглядный, но в реальных Go-проектах вы, конечно, можете использовать и более удобные методы. Например, многие выбирают готовые middleware-библиотеки, такие как пакет github.com/rs/cors
или средства, встроенные в популярные фреймворки (Fiber, Gin, Echo и др.). Эти решения позволяют одной строкой включить поддержку CORS и гибко её настроить (вплоть до динамического списка разрешённых доменов). Однако под капотом они делают то же самое – выставляют нужные заголовки и обрабатывают preflight, как мы сделали вручную.
Заключение
Мы разобрали, что такое CORS, почему он возник и как работает на уровне браузера и сервера. Кросс-доменные запросы – неотъемлемая часть современных веб-приложений, и понимание механизма CORS позволяет разработчикам избегать ошибок и защищать свои системы. Для начинающих важно уяснить: CORS настраивается на стороне сервера – именно сервер решает, кому дать доступ к своим ресурсам. Браузер же строго следует указаниям сервера, блокируя всё, что не разрешено.
При работе с CORS придерживайтесь принципа минимально необходимого доступа: открывайте конкретные домены, методы и заголовки, которые требуются вашему приложению, и избегайте излишне широких правил в продакшне. В учебных проектах можно экспериментировать с разными настройками, чтобы понять поведение браузера, но всегда помните о безопасности в реальных условиях.
Надеемся, эта статья помогла прояснить суть CORS и выручит вас при отладке тех самых «пресловутых» ошибок в консоли. Настроив CORS правильно, вы сможете безопасно интегрировать ваши фронтенд и бэкенд, а пользователи – беспрепятственно пользоваться функционалом вашего веб-приложения. Успехов в разработке!
You must be logged in to post a comment.