Содержание урока · 14 разделов
- fiber cors: Same-Origin Policy и запрос с чужого origin
- Простые запросы и preflight: как браузер спрашивает разрешение
- Словарь CORS-заголовков
- cors настройки go: Config по полям
- AllowOrigins: список, субдомены и паника на опечатке
- AllowCredentials: cookies через чужой origin
- Vary, MaxAge и AllowPrivateNetwork: тонкости ответа
- Порядок слоёв: helmet → cors → страж
- Helmet-подобные заголовки: в Fiber v3 есть встроенный helmet
- Дефолты helmet.New()
- Запуск, проверка curl-ом и что дальше
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
fiber cors: Same-Origin Policy и запрос с чужого origin#
Same-Origin Policy — базовая политика безопасности браузера: скрипт, загруженный с одного origin, не вправе читать ответы, пришедшие с другого. Origin — это тройка «схема + хост + порт»: http://localhost:3000 и http://localhost:8080 — два разных origin, даже если живут на одной машине; https://example.com и http://example.com — тоже разные, ведь схема и порт входят в origin. Политика защищает пользователя: страница банка не должна уметь дёргать API почтового сервиса с вашими cookies и читать результат.
Проблема всплывает, когда фронтенд и API вы разворачиваете отдельно: SPA крутится на localhost:3000, API — на localhost:8080, и без дополнительных заголовков браузер заблокирует чтение ответов — по умолчанию запрещено всё, что не совпадает по схеме, хосту и порту.
CORS — Cross-Origin Resource Sharing — способ сервера честно объявить браузеру: ответы для этого origin читать можно. Формулировка из документации Fiber переворачивает интуицию: CORS — не защитный механизм, он лишь ослабляет same-origin policy, позволяя кросс-доменным запросам состояться. Настоящая защита — молчаливый запрет браузера, который действует до всякого CORS; ваш слой решает, какую часть запрета снять. Отсюда два вывода, к которым мы вернёмся: список разрешённых origin — это список дверей, которые вы открыли, и он должен быть минимальным; а не-браузерные клиенты CORS игнорируют целиком.
Простые запросы и preflight: как браузер спрашивает разрешение#
Кросс-доменные запросы делятся на два класса. Простые (simple) — исторически безопасные: методы GET, HEAD и POST, обычные заголовки вроде Accept и Content-Type из тройки application/x-www-form-urlencoded, multipart/form-data, text/plain. Такой запрос браузер отправляет сразу, а заголовки CORS смотрит уже в ответе: если Access-Control-Allow-Origin разрешает — JS получает данные; если нет — ответ выбрасывается, и в консоли появляется ошибка.
Всё остальное уходит через preflight. Перед запросом с методами PUT, DELETE, PATCH, QUERY, с кастомными заголовками вроде X-API-Key или с Content-Type: application/json браузер сначала отправляет OPTIONS — предварительный запрос без тела — с заголовками Origin (откуда уйдёт реальный запрос), Access-Control-Request-Method (каким методом) и Access-Control-Request-Headers (какие заголовки понесёт). Сервер отвечает статусом без тела и списком разрешений: Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Max-Age. Браузер сверяет ответ с планами: совпало — отправляет реальный запрос; нет — реальный запрос не уходит вовсе.
Отсюда следствие, которое потом нас укусит: preflight не несёт ни cookies, ни кастомных заголовков — браузер не может знать заранее, что сервер разрешит. Запрос OPTIONS выглядит для вашего API как полностью анонимный, и любой слой, требующий ключ или сессию, обязан пускать его до CORS-слоя. Порядок регистрации из шестого урока здесь снова решает всё.
Словарь CORS-заголовков#
| Заголовок | Кто шлёт | Что объявляет |
|---|---|---|
| Origin | браузер, каждый CORS-запрос | origin страницы, с которой уходит запрос |
| Access-Control-Request-Method | браузер, только preflight | метод будущего реального запроса |
| Access-Control-Request-Headers | браузер, только preflight | заголовки, которые понесёт реальный запрос |
| Access-Control-Allow-Origin | сервер | какой origin вправе читать ответ |
| Access-Control-Allow-Credentials | сервер | можно ли отправлять cookies и другие учётные данные |
| Access-Control-Allow-Methods / -Headers | сервер, preflight | разрешённые метод и заголовки реального запроса |
| Access-Control-Max-Age | сервер, preflight | сколько секунд кэшировать разрешение |
Минимальный слой CORS: cors.New() и дефолты
GOpackage main
import (
"log"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/cors"
)
func main() {
app := fiber.New()
// слой CORS на всё приложение: без аргумента — ConfigDefault
app.Use(cors.New())
app.Get("/api/notes", func(c fiber.Ctx) error {
return c.JSON(fiber.Map{"items": []string{"первая"}})
})
log.Fatal(app.Listen(":8080"))
}Знакомый по седьмому уроку рисунок: New(config ...Config) fiber.Handler — фабрика, конфиг опционален. ConfigDefault из config.go: AllowOrigins: []string{""}, AllowMethods: GET, POST, HEAD, PUT, DELETE, PATCH, QUERY, AllowCredentials: false, AllowHeaders и ExposeHeaders пусты, MaxAge: 0. Метод QUERY — не опечатка: в constants.go это константа fiber.MethodQuery со значением QUERY и комментарием RFC 10008 — новый HTTP-метод для запросов с телом, который Fiber держит в дефолтном списке CORS. Звёздочка в AllowOrigins означает «читать можно любому origin»: на любой запрос с Origin браузер получит Access-Control-Allow-Origin: .
cors настройки go: Config по полям#
Все настройки CORS в Go на Fiber живут в одной структуре cors.Config — разберём поля по исходникам тега v3.5.0, от частых к редким.
AllowOrigins []string — список origin, дефолт — []string{"*"}. Тонкость из кода: пустой список при AllowOriginsFunc == nil тоже означает «разрешить всем» — условие allowAllOrigins := len(cfg.AllowOrigins) == 0 && cfg.AllowOriginsFunc == nil; а пустой список с заданной функцией — наоборот, никого не пускает без проверки функцией (заметка в документации). AllowOriginsFunc func(origin string) bool — динамическая проверка, когда origin надо сверять с базой; ей передаются сериализованные origin и литерал "null".
AllowMethods — список методов для ответа на preflight (дефолт выше). AllowHeaders — какие заголовки реального запроса разрешены; по умолчанию пусто, и тогда middleware возвращает эхом то, что попросил браузер. ExposeHeaders — зеркальная вещь для ответов: по умолчанию JS видит лишь базовый набор вроде Cache-Control, Content-Type и Last-Modified, а так вы разрешаете читать свои заголовки — например X-Response-Time из седьмого урока.
MaxAge int — на сколько секунд браузер запомнит preflight-ответ. Ноль — заголовок Access-Control-Max-Age не ставится, документация уточняет: браузер возьмёт свои 5 секунд по умолчанию. Отрицательное значение ставит заголовок в 0 — кэш preflight отключён полностью, каждый кросс-доменный запрос начинается с OPTIONS. DisableValueRedaction bool — про логи и паники: по умолчанию false, и невалидные origin в сообщениях маскируются строкой [redacted]. Next func(c fiber.Ctx) bool — стандартный скипер: вернул true — слой пропущен.
Проверьте себя: ответ на preflight
Приложение с app.Use(cors.New()) без аргументов получило preflight: OPTIONS с Origin: http://localhost:3000 и Access-Control-Request-Method: POST. Что вернёт сервер?
AllowOrigins: список, субдомены и паника на опечатке#
Рабочий конфиг почти всегда — явный список. Механика из cors.go: входящий Origin приводится к нижнему регистру и сравнивается с разрешёнными; при совпадении Access-Control-Allow-Origin получает точное значение Origin из запроса — не звёздочку. Именно поэтому на простые запросы при конфиге-списке слой добавляет Vary: Origin: ответ зависит от заголовка запроса, и кэш не должен отдавать одному origin ответ, сгенерированный для другого.
Формат каждого элемента проверяет normalizeOrigin: url.Parse, запрет пути, query, фрагмента и userinfo; завершающий слэш разрешён и отбрасывается, схема и хост приводятся к нижнему регистру. Невалидный элемент — не ошибка во время запроса, а паника при создании слоя: [CORS] Invalid origin format in configuration: ... — приложение не поднимется с опечаткой вроде http:/localhost:3000.
Поддержаны субдомены: элемент https://*.example.com разрешает любой уровень вложенности — https://sub.example.com и https://sub.sub.example.com — но не сам https://example.com. Отдельная история — литерал null: браузер шлёт Origin: null для file://-страниц, data:-URL и sandboxed iframes; cors.go этот случай распознаёт, но статический список null не примет — normalizeOrigin отвергнет строку без схемы и хоста, такой origin разрешается только через AllowOriginsFunc, если ваша логика того хочет.
Что уедет в ответе на preflight?
Прочитайте код и выберите, что он выведет:
app.Use(cors.New(cors.Config{
AllowOrigins: []string{"http://localhost:3000"},
}))
// Запрос из браузера:
// OPTIONS /api/notes
// Host: localhost:8080
// Origin: http://localhost:3000
// Access-Control-Request-Method: POSTAllowCredentials: cookies через чужой origin#
AllowCredentials — самое строгое поле. Оно отвечает на вопрос: можно ли запросу нести учётные данные — cookies, TLS-сертификат клиента, заголовки авторизации при fetch с credentials: "include". Со стороны Fiber поле делает две вещи. Первая — в setSimpleHeaders при разрешённом конкретном origin ставится Access-Control-Allow-Credentials: true; при звёздочке — не ставится вовсе.
Вторая — защита на старте. В New() стоит проверка: AllowCredentials вместе с wildcard «» (или с пустым списком без функции — он тоже означает «все») приводит к панике ещё до запуска сервера: [CORS] Configuration error: When 'AllowCredentials' is set to true, 'AllowOrigins' cannot contain a wildcard origin ''. Please specify allowed origins explicitly or adjust 'AllowCredentials' setting. Причина в спецификации: комбинация «звёздочка + credentials» запрещена браузерами и уязвима — любой сайт смог бы отправлять запросы с cookies пользователя к вашему API. Ошибка конфигурации ловится при регистрации, а не в проде — фабрика из седьмого урока снова оправдывает себя.
Если список origin динамический, берите AllowOriginsFunc: функция получает origin и возвращает bool, middleware поставит Access-Control-Allow-Origin с этим значением. Документация предупреждает жёстко: никогда не возвращайте true для всех origin — особенно при AllowCredentials: это обходной путь мимо запрета звёздочки, и вы потеряете защиту, которую паника пыталась обеспечить. Задали и AllowOrigins, и AllowOriginsFunc — слой предупредит в лог: [CORS] Both 'AllowOrigins' and 'AllowOriginsFunc' have been defined.
Проверьте себя: конфиг, который не поднимется
Что произойдёт при регистрации app.Use(cors.New(cors.Config{AllowOrigins: []string{"*"}, AllowCredentials: true}))?
Конфиг для связки SPA на :3000 и API на :8080
GO// API на :8080 разрешает только фронтенд с localhost:3000
app.Use(cors.New(cors.Config{
// единственный разрешённый origin — адрес SPA
AllowOrigins: []string{"http://localhost:3000"},
// реальный запрос понесёт JSON и заголовок стража из седьмого урока
AllowHeaders: []string{"Content-Type", "X-API-Key"},
// методов хватит на CRUD заметок
AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},
// fetch(..., { credentials: "include" }) отправит cookies
AllowCredentials: true,
// preflight-разрешение кэшируется браузером на час
MaxAge: 3600,
}))Что увидит браузер. На preflight: 204, Access-Control-Allow-Origin: http://localhost:3000, Access-Control-Allow-Credentials: true, Access-Control-Allow-Methods: GET, POST, PUT, DELETE, Access-Control-Allow-Headers: Content-Type, X-API-Key и Access-Control-Max-Age: 3600 — час без повторных OPTIONS. На простом GET с этим Origin: только Access-Control-Allow-Origin и Access-Control-Allow-Credentials — методы и заголовки на простые запросы не ставятся, в cors.go это видно по ветке simple request с комментарием Omit allowMethods and allowHeaders, only used for pre-flight requests. Content-Type: application/json и X-API-Key перечислены не зря: оба делают запрос «непростым» и вызывают preflight.
Vary, MaxAge и AllowPrivateNetwork: тонкости ответа#
Три детали, отличающие понимание от копипасты. Первая — Vary. cors.go добавляет его почти в каждой ветке: на простые запросы при конфиге-списке — Vary: Origin; на preflight — Vary: Access-Control-Request-Method, Access-Control-Request-Headers и Origin (плюс Access-Control-Request-Private-Network, если включён AllowPrivateNetwork); даже запрос без Origin — «вне CORS» — получает Vary: Origin, если не разрешено всем. Это указание кэшам и прокси: ответ зависел от заголовков запроса, нельзя переиспользовать его для другого Origin — иначе один фронтенд получит из кэша чужие разрешения.
Вторая — MaxAge, уже разобранное: ноль — заголовка нет и браузер живёт своими 5 секундами, отрицательное — кэш запрещён заголовком 0, положительное — столько-то секунд.
Третья — AllowPrivateNetwork, по умолчанию false. Это ответ на Chrome Private Network Access: запрос из публичного сайта к серверу в приватной сети — в том числе к localhost — браузер объявляет в preflight заголовком Access-Control-Request-Private-Network: true, и сервер должен согласиться ответом Access-Control-Allow-Private-Network: true. Наша связка «SPA и API на localhost» флаг не требует — публичная граница между origin-ами не пересекается; но если страница из интернета дёргает ваш локальный dev-сервер, вы знаете, за какой переключатель отвечает.
Пустой AllowHeaders: эхо из запроса
Прочитайте код и выберите, что он выведет:
app.Use(cors.New(cors.Config{
AllowOrigins: []string{"http://localhost:3000"},
// AllowHeaders не задан — дефолтный пустой список
}))
// Preflight из браузера:
// OPTIONS /api/notes
// Origin: http://localhost:3000
// Access-Control-Request-Method: POST
// Access-Control-Request-Headers: content-type, x-api-keyПорядок слоёв: helmet → cors → страж#
Соберём практику: SPA на localhost:3000 — dev-сервер Vite или статика fiber из пятого урока — и API на localhost:8080. Регистрация слоёв в API — это снова луковица из шестого урока, и два решения здесь обязательны.
Первое: cors встаёт РАНЬШЕ стража requireAPIKey из седьмого урока. Preflight не несёт ни cookies, ни X-API-Key — браузер не отправляет учётные данные в предварительный запрос. Если страж зарегистрирован раньше, он первым видит OPTIONS, не находит заголовок ключа и отвечает 401 — preflight провален, реальный запрос браузер даже не начнёт. Слой CORS, напротив, отвечает на preflight сам и пропускает простые запросы дальше со свежими заголовками.
Второе: helmet встаёт раньше cors. Preflight-ветка cors завершается SendStatus(204) без c.Next() — слои, зарегистрированные ПОСЛЕ cors, на preflight-ответ не работают вовсе: луковица дальше не разворачивается. Helmet ставит свои заголовки ДО вызова c.Next(), поэтому, стоя снаружи, он украшает и обычные ответы, и preflight; внутри — и preflight уйдёт без шлема. Правило собирается в цепочку: helmet → cors → логи и страж → маршруты. Обе части проверяются по исходникам: в cors.go — return c.SendStatus(fiber.StatusNoContent) сразу после заголовков; в helmet.go — все c.Set до return c.Next().
Каркас API: шлем, CORS, страж, маршруты
GOpackage main
import (
"log"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/cors"
"github.com/gofiber/fiber/v3/middleware/helmet"
"github.com/gofiber/fiber/v3/middleware/logger"
)
// страж из седьмого урока: без X-API-Key группа закрыта
func requireAPIKey(key string) fiber.Handler {
return func(c fiber.Ctx) error {
if c.Get("X-API-Key") != key {
return c.Status(fiber.StatusUnauthorized).JSON(
fiber.Map{"error": "неверный API-ключ"},
)
}
c.Locals("user", "spa-client")
return c.Next()
}
}
func main() {
app := fiber.New()
// 1: helmet раньше cors — его заголовки попадают и на preflight
app.Use(helmet.New())
// 2: cors раньше стража — preflight не несёт X-API-Key
app.Use(cors.New(cors.Config{
AllowOrigins: []string{"http://localhost:3000"},
AllowHeaders: []string{"Content-Type", "X-API-Key"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},
AllowCredentials: true,
MaxAge: 3600,
}))
// 3: лог и защищённая группа из седьмого урока
app.Use(logger.New())
api := app.Group("/api", requireAPIKey("s3cret"))
api.Get("/notes", func(c fiber.Ctx) error {
return c.JSON(fiber.Map{"items": []string{"первая", "вторая"}})
})
api.Post("/notes", func(c fiber.Ctx) error {
return c.Status(fiber.StatusCreated).JSON(fiber.Map{"ok": true})
})
// 4: health-check вне группы
app.Get("/healthz", func(c fiber.Ctx) error {
return c.JSON(fiber.Map{"ok": true})
})
log.Fatal(app.Listen(":8080"))
}Каркас развивает седьмой урок: requireAPIKey и группа /api прежние, добавились два внешних слоя. Лог запроса браузера на POST /api/notes выглядит так: OPTIONS — 204 и только заголовки, страж и хендлер не вызывались; затем POST с X-API-Key — страж пускает, пишет пользователя в locals, хендлер отвечает 201. Запрос с Origin: http://evil.example получит ответ без Access-Control-Allow-Origin — браузер откажется его читать; curl же без Origin вообще получит данные, потому что для слоя это запрос вне CORS. Оба поведения наблюдаемы в терминале — рецепт в конце урока.
Helmet-подобные заголовки: в Fiber v3 есть встроенный helmet#
Helmet — концепт из мира Node.js: middleware, которое надевает «шлем» на каждый ответ, проставляя набор защитных заголовков. Естественный вопрос — есть ли оно в Fiber. Сверим с тегом v3.5.0: да, в комплекте v3 есть встроенный пакет github.com/gofiber/fiber/v3/middleware/helmet — штатный слой каталога middleware с привычными Config и ConfigDefault, а не сторонний аддон. app.Use(helmet.New()) — и ответы уходят с защитными заголовками.
Что стоит по умолчанию: X-Content-Type-Options: nosniff — запрет MIME-sniffing, браузер не станет угадывать тип содержимого поверх вашего Content-Type; X-Frame-Options: SAMEORIGIN — страницу нельзя встроить в iframe чужого сайта, приём кликджекинга (значение DENY — жёстче, запретит даже встраивание с самого себя); Referrer-Policy: no-referrer — адреса страниц не утекают в Referer; три Cross-Origin-заголовка — Opener-Policy: same-origin, Resource-Policy: same-origin, Embedder-Policy: require-corp — изоляция окон и ресурсов между origin-ами; плюс X-DNS-Prefetch-Control: off, X-Download-Options: noopen, X-Permitted-Cross-Domain-Policies: none и Origin-Agent-Cluster: ?1.
Два дефолта — со смыслом. X-XSS-Protection: 0 — ровно ноль, отключение: устаревший браузерный фильтр XSS auditor сам создавал уязвимости, и современная рекомендация — выключить его, а защиту строить на CSP. А Content-Security-Policy по умолчанию пуст: helmet не ставит заголовок, пока вы не зададите строку, потому что универсального CSP не существует — белые списки ресурсов у каждого приложения свои. Туда же Permissions-Policy (пусто) и Strict-Transport-Security: HSTSMaxAge по умолчанию 0, заголовок ставится только на HTTPS-запросах — c.Secure() — и только при положительном MaxAge; строку max-age=...; includeSubDomains слой собирает один раз в теле фабрики — тот же приём «подготовка до return», что мы разбирали в седьмом уроке.
helmet.New() с настройкой CSP и HSTS
GO// Шлем на всё приложение: CSP и HSTS включаются явно
app.Use(helmet.New(helmet.Config{
// скрипты и стили — только с самого приложения
ContentSecurityPolicy: "default-src 'self'",
// HSTS: два года только для HTTPS, включая субдомены
HSTSMaxAge: 63072000,
}))Правило простое: поле пустое — заголовка нет, строка непустая — ставится. ContentSecurityPolicy уедет как Content-Security-Policy, а при CSPReportOnly: true — как Content-Security-Policy-Report-Only: браузер сообщает о нарушениях, но не блокирует — удобный режим обкатки политики. HSTSMaxAge 63072000 — классические два года; HSTSExcludeSubdomains: true уберёт includeSubDomains, HSTSPreloadEnabled добавит preload. Исходники паникуют на несовместимых настройках: helmet: HSTSPreloadEnabled requires HSTSExcludeSubdomains to be false и helmet: HSTSMaxAge must be greater than or equal to 0 при отрицательном значении.
Дефолты helmet.New()#
| Заголовок | Значение по умолчанию | От чего защищает |
|---|---|---|
| X-Content-Type-Options | nosniff | подмена типов: браузер не угадывает MIME поверх Content-Type |
| X-Frame-Options | SAMEORIGIN | кликджекинг: встраивание страницы в чужой iframe |
| Referrer-Policy | no-referrer | утечка адресов страниц в заголовке Referer |
| Cross-Origin-Opener-Policy | same-origin | изоляция окна: чужие origin не получают доступ к window |
| X-XSS-Protection | 0 | отключение устаревшего браузерного фильтра XSS |
| Content-Security-Policy | не ставится (пусто) | белые списки ресурсов — задаётся вручную под приложение |
Проверьте себя: чего в шлеме нет
Какой заголовок НЕ появится в ответе после app.Use(helmet.New()) без аргументов?
Соберите регистрацию слоёв API
Расставьте строки: сначала шлем, потом CORS, затем лог, страж и маршруты; запуск — последним.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Допишите CORS-конфиг
Вставьте пропуски: поле списка origin, заголовок стража, разрешение cookies и срок кэша preflight.
Access-Control-Allow-Origin отвечает на вопрос «какому фронтенду браузер разрешит читать ответы», а не «кто допускается к API». curl, серверные клиенты, боты и библиотеки не исполняют Same-Origin Policy: они шлют запросы без Origin и без всяких CORS-заголовков, и слой пропускает их как запросы «вне CORS». Список AllowOrigins не прячет данные от мира — он лишь снимает браузерное ограничение для избранных. Поэтому страж requireAPIKey("s3cret") из седьмого урока остаётся обязательным: CORS и аутентификация решают разные задачи и работают в паре, а не вместо друг друга.
Слой бережёт ваши логи: невалидные origin в панике Invalid origin format и в предупреждениях маскируются строкой [redacted] — конфигурация может содержать адреса стейджинга и внутренних сервисов, которые не должны светиться в алертах. Для отладки задайте DisableValueRedaction: true и увидите исходные значения. Ещё два предупреждения log.Warn: заданы и AllowOrigins, и AllowOriginsFunc одновременно; а также AllowOrigins is set to allow all origins, AllowOriginsFunc will not be used — функция игнорируется при звёздочке.
Словарь урока
Запуск, проверка curl-ом и что дальше#
Соберите каркас в main.go и прогоните три сценария. Первый — простой запрос с Origin: curl -i -H "Origin: http://localhost:3000" http://localhost:8080/api/notes — в ответе 200 и заголовки Access-Control-Allow-Origin: http://localhost:3000 и Access-Control-Allow-Credentials: true. Тот же curl без -H Origin вернёт данные без единого CORS-заголовка — для слоя это запрос вне CORS; вот почему проверку делают с Origin, иначе вы тестируете не то. Второй — preflight: curl -i -X OPTIONS http://localhost:8080/api/notes -H "Origin: http://localhost:3000" -H "Access-Control-Request-Method: POST" -H "Access-Control-Request-Headers: Content-Type, X-API-Key" — 204, полный набор разрешений, Access-Control-Max-Age: 3600 и шлем: X-Content-Type-Options: nosniff. Третий — чужой origin: с Origin: http://evil.example заголовка Access-Control-Allow-Origin в ответе не будет вовсе — молчаливый отказ, на котором браузер и спотыкается. А запрос без X-API-Key покажет 401 от стража седьмого урока — CORS позволил браузеру дотянуться, но не открыл дверь.
Дальше по плану — девятый урок про JSON: байты, которые едут под всеми этими заголовками, ответы c.JSON и разбор входящих запросов. Освежить стража и c.Locals — в седьмом уроке, механику луковицы и порядка слоёв — в шестом, полный план — на странице курса Fiber. В браузерной песочнице сайта сетевые приложения не запускаются — runner-ов в уроках Fiber нет, проверяйте примеры в терминале с локальным Go.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Same-Origin Policy запрещает браузеру читать ответы чужого origin; CORS — серверное разрешение снять запрет для перечисленных origin. CORS — не защита API: не-браузерные клиенты его игнорируют.
- Preflight — предварительный OPTIONS с Origin, Access-Control-Request-Method и Access-Control-Request-Headers; не несёт cookies и ключей. Fiber отвечает 204 No Content и набором разрешений.
- cors.Config: AllowOrigins (дефолт со звёздочкой; пустой список без Func — тоже все), AllowMethods GET, POST, HEAD, PUT, DELETE, PATCH, QUERY, AllowHeaders, ExposeHeaders, MaxAge (0 — без заголовка и 5 секунд браузера; отрицательный — кэш отключён), AllowCredentials, AllowPrivateNetwork.
- AllowCredentials: true со звёздочкой — паника при регистрации; для разрешённого origin ставятся Access-Control-Allow-Credentials: true и точное значение Access-Control-Allow-Origin.
- Порядок слоёв: helmet раньше cors — preflight-ответ (204 без c.Next()) не доходит до слоёв ниже; cors раньше стража — preflight не несёт X-API-Key.
- helmet — штатный middleware/helmet в v3: nosniff, SAMEORIGIN, no-referrer и серия Cross-Origin-заголовков по умолчанию; CSP, Permissions-Policy и HSTS включаются явно, HSTS — только на c.Secure() и при HSTSMaxAge > 0.
- Vary: Origin и Access-Control-Request-* — указание кэшам; curl без Origin уходит мимо CORS-заголовков, тестировать нужно с -H "Origin: ...".
Домашнее задание
Развейте каркас урока: добавьте в AllowOrigins второй origin, например http://localhost:5173, и проверьте curl-ом, что Access-Control-Allow-Origin повторяет Origin из запроса, а не выдаёт весь список; соберите helmet-конфиг с ContentSecurityPolicy "default-src 'self'" и CSPReportOnly: true и найдите Content-Security-Policy-Report-Only в ответе; включите AllowPrivateNetwork и отработайте preflight с заголовком Access-Control-Request-Private-Network: true; зарегистрируйте страж раньше cors и в комментарии объясните, почему preflight сломается; наконец задайте MaxAge: -1 и убедитесь, что Access-Control-Max-Age в ответе равен 0.
Личные заметкиПусто
Вопросы и ответы по уроку
Почему браузер шлёт OPTIONS, хотя я вызываю только fetch с POST?
Запрос «непростой»: Content-Type: application/json и/или кастомные заголовки вроде X-API-Key выводят его из списка простых. Браузер сначала спрашивает разрешение preflight-ом — OPTIONS с Origin и Access-Control-Request-Method, — дожидается 204 с Access-Control-Allow-*, и только потом отправляет реальный POST. Заголовок Access-Control-Max-Age сокращает число OPTIONS.
Access-Control-Allow-Origin со звёздочкой — это дыра в безопасности?
Само по себе нет: звёздочка лишь разрешает любому фронтенду читать ответы вашего API из браузера. Опасна комбинация с AllowCredentials: true — cookies пользователя уходят с запросом; Fiber паникует при такой конфигурации ещё на старте. И помните: звёздочка ничего не запрещает curl-клиентам — от них защищает аутентификация, а не CORS.
Есть ли в Fiber v3 встроенный helmet?
Да, middleware/helmet в комплекте v3 — штатный слой с Config и ConfigDefault. По умолчанию ставит X-Content-Type-Options: nosniff, X-Frame-Options: SAMEORIGIN, Referrer-Policy: no-referrer и серию Cross-Origin-заголовков; Content-Security-Policy и HSTS включаются явной настройкой. Регистрируйте его раньше cors, чтобы заголовки попадали и на preflight-ответы.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Gin: самый популярный веб-фреймворк Go
Курс Gin для Go: маршрутизация, binding, валидация, middleware, JWT, WebSocket, тесты и деплой. 25 уроков с кодом и итоговым REST API.
- Уверенно владеть gin.Engine: маршруты, группы, версии
- Привязывать запросы к структурам (ShouldBindJSON и др.)
- Писать middleware и использовать встроенные
- Строить аутентификацию на JWT