Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 8 из 25 ~18 мин +50 XP

CORS и безопасность заголовков

Тема «fiber cors» — про договор между браузером и вашим API. До сих пор мы проверяли сервис curl-ом, и ни один слой не спрашивал, откуда пришёл запрос: у curl нет origin. Браузер устроен строже: он подчиняется Same-Origin Policy и не даст скрипту страницы с localhost:3000 прочитать ответ API на localhost:8080, если сервер явно не разрешит. Разберём настройки CORS в Go и Fiber: preflight-запрос OPTIONS и заголовки Access-Control-Allow-Origin, Access-Control-Allow-Credentials, Access-Control-Allow-Methods, Access-Control-Allow-Headers; сверим каждое поле Config с исходниками тега v3.5.0, соберём связку SPA на :3000 плюс API на :8080 и добавим helmet-подобные заголовки безопасности — в Fiber v3 для них есть встроенный пакет middleware/helmet.

Содержание урока · 14 разделов

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() и дефолты

GO
package 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: POST

AllowCredentials: 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, страж, маршруты

GO
package 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-Optionsnosniffподмена типов: браузер не угадывает MIME поверх Content-Type
X-Frame-OptionsSAMEORIGINкликджекинг: встраивание страницы в чужой iframe
Referrer-Policyno-referrerутечка адресов страниц в заголовке Referer
Cross-Origin-Opener-Policysame-originизоляция окна: чужие origin не получают доступ к window
X-XSS-Protection0отключение устаревшего браузерного фильтра XSS
Content-Security-Policyне ставится (пусто)белые списки ресурсов — задаётся вручную под приложение

Проверьте себя: чего в шлеме нет

Какой заголовок НЕ появится в ответе после app.Use(helmet.New()) без аргументов?

Соберите регистрацию слоёв API

Расставьте строки: сначала шлем, потом CORS, затем лог, страж и маршруты; запуск — последним.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Допишите CORS-конфиг

Вставьте пропуски: поле списка origin, заголовок стража, разрешение cookies и срок кэша preflight.

app.Use(cors.New(cors.Config{ // единственный разрешённый origin фронтенда : []string{"http://localhost:3000"}, // заголовки реального запроса, которые готов принять сервер AllowHeaders: []string{"Content-Type", ""}, // разрешаем cookies в кросс-доменных запросах AllowCredentials: , // кэшировать preflight-разрешение на час : 3600, }))

Словарь урока

Термин

preflight

нажмите, чтобы перевернуть
Определение

Предварительный OPTIONS-запрос браузера перед «непростым» кросс-доменным запросом: Origin плюс Access-Control-Request-Method (и Request-Headers). Не несёт cookies и кастомных заголовков; ответ сервера — 204 с разрешениями.

1 / 6

Запуск, проверка 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.

Личные заметкиПусто
Хранятся только в вашем браузере (Fiber · CORS и безопасность заголовков)

Вопросы и ответы по уроку

Почему браузер шлёт 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-ответы.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

Следующая ступень учебного пути

Gin25 уроков~13 чпрактика25 уроков уже доступны

Gin: самый популярный веб-фреймворк Go

Курс Gin для Go: маршрутизация, binding, валидация, middleware, JWT, WebSocket, тесты и деплой. 25 уроков с кодом и итоговым REST API.

  • Уверенно владеть gin.Engine: маршруты, группы, версии
  • Привязывать запросы к структурам (ShouldBindJSON и др.)
  • Писать middleware и использовать встроенные
  • Строить аутентификацию на JWT