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

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

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

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

CORS и безопасность

Фронтенд трекера переехал на отдельный дев-сервер localhost:5173, и браузер перестал отдавать JS ответы API на 8080 — при том, что curl работает идеально. Это same-origin policy: браузер по умолчанию не читает ответы чужого origin, и CORS — механизм, которым сервер выписывает согласие. Настроим gin cors через gin-contrib/cors: конфиг AllowOrigins, AllowMethods и AllowHeaders, preflight OPTIONS, AllowCredentials против звёздочки, AllowOriginFunc и паттерны для продакшена. Затем продолжим безопасность gin за пределами CORS: звено заголовков HSTS, nosniff, X-Frame-Options и CSP в духе [кастомных middleware](/courses/gin/lessons/07-kastomnye-middleware/), и обзор CSRF — почему SameSite из [урока про cookie](/courses/gin/lessons/14-cookie-sessii/) — первая линия, а CORS — не защита. Simple-запросы и multipart вспомним из [урока про загрузку файлов](/courses/gin/lessons/13-fajly/), Bearer-заголовок из [урока про JWT](/courses/gin/lessons/15-jwt/), кэширование — из [урока про статику](/courses/gin/lessons/11-statika/).

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

gin cors: same-origin policy и зачем согласие#

Origin — это схема плюс хост плюс порт: http://localhost:5173 и http://localhost:8080 — разные origin-а, хотя разница в одном числе. Same-origin policy — правило браузера: скрипт страницы может свободно читать ответы только своего origin-а; ответ чужого origin-а он получает, но не имеет права увидеть. Отсюда классическая сцена: фронтенд на 5173 зовёт fetch("http://localhost:8080/api/v1/todos") и в консоли всплывает «blocked by CORS policy», хотя curl и Postman тот же адрес отвечают без проблем — браузер там не участвует, а CORS — это правило именно браузера. Важно понять, что CORS — не запрет на отправку, а запрет на чтение. Запрос браузер выполнит и даже получит ответ — сервер отработает, база изменится, — но JS-код ответа не увидит, пока сервер сам не разрешит: заголовком Access-Control-Allow-Origin с именем origin-а клиента. Это принципиально меняет представление о защите: CORS не закрывает API от чужих клиентов — curl-то работает, — он лишь регулирует, какие веб-страницы вправе читать ответы. Защита самого API — токены из прошлого урока и звенья; CORS — про дружбу с честным фронтендом. Серверная часть темы — пакет github.com/gin-contrib/cors: go get github.com/gin-contrib/cors, cors.New(cors.Config{...}) — одно звено на r.Use, и дальше вся работа происходит в заголовках ответа. Связку gin cors надо настроить один раз и понимать построчно, потому что ломается она в самых неожиданных местах: то preflight не видит Authorization, то AllowCredentials конфликтует со звёздочкой, то CDN кэширует ответ одного origin-а для всех. Разберём всё по порядку: сначала механика простых и preflight-запросов, потом конфиг, потом безопасность за пределами CORS.

Preflight: OPTIONS до настоящего запроса#

Браузер делит cross-origin запросы на два класса. Simple-запросы — GET, HEAD и POST с Content-Type из тройки text/plain, application/x-www-form-urlencoded и multipart/form-data, без авторских заголовков — браузер отправляет сразу, прикладывая заголовок Origin; сервер в ответе либо ставит Access-Control-Allow-Origin, и JS прочитает ответ, либо молчит, и браузер скрывает ответ, хотя обработка состоялась. Multipart из урока про загрузку файлов попадает в simple-класс именно из-за этого списка. Всё остальное — POST с Content-Type: application/json, PUT, DELETE, PATCH, любой запрос с Authorization или X-* заголовками — требует preflight: перед настоящим запросом браузер шлёт OPTIONS с заголовками Origin, Access-Control-Request-Method (что собирается сделать) и Access-Control-Request-Headers (какие заголовки хочет поставить). Сервер обязан ответить 204 без тела и перечислить разрешения: Access-Control-Allow-Origin — конкретный origin клиента, Access-Control-Allow-Methods — список методов, Access-Control-Allow-Headers — список заголовков, Access-Control-Max-Age — сколько секунд браузеру можно не спрашивать снова. Только после удачного preflight уходит настоящий запрос — и CORS-заголовки должны быть и на нём, иначе ответ всё равно скрыт. Два заголовка, о которых забывают. Access-Control-Expose-Headers: сервер по умолчанию показывает JS-у только Cache-Control, Content-Language, Content-Type, Expires, Last-Modified и Pragma — ваш X-Total-Count для пагинации JS не увидит, пока вы его не откроете. Access-Control-Allow-Credentials: true — разрешает браузеру прикладывать cookie и Authorization к cross-origin запросу; без него fetch с credentials: "include" получит cookie, но ответ будет скрыт. И Vary: Origin — когда заголовок зависит от присланного Origin, кэши обязаны знать, что нельзя отдавать всем ответ, собранный для одного origin-а; gin-contrib/cors ставит его сам, но понимать причину нужно до первого кэширующего прокси.

gin-contrib/cors: конфиг на один r.Use

GO
package main

import (
        "net/http"
        "time"

        "github.com/gin-contrib/cors"
        "github.com/gin-gonic/gin"
)

func main() {
        gin.SetMode(gin.ReleaseMode)
        r := gin.Default()

        r.Use(cors.New(cors.Config{
                AllowOrigins: []string{
                        "http://localhost:5173",
                        "https://goma.dev",
                },
                AllowMethods: []string{
                        http.MethodGet, http.MethodPost,
                        http.MethodPut, http.MethodDelete,
                        http.MethodOptions,
                },
                AllowHeaders:     []string{"Origin", "Content-Type", "Authorization"},
                ExposeHeaders:    []string{"Content-Length", "X-Total-Count"},
                AllowCredentials: true,
                MaxAge:           12 * time.Hour,
        }))

        api := r.Group("/api/v1")
        api.GET("/todos", listTodos)
        api.POST("/todos", createTodo)

        if err := r.Run(":8080"); err != nil {
                panic(err)
        }
}

go get github.com/gin-contrib/cors. Одно звено до маршрутов — cors перехватит OPTIONS и допишет заголовки в обычные ответы. AllowOrigins — белый список origin-ов целиком, со схемой и портом. AllowHeaders перечисляет то, что клиент шлёт в preflight: без Authorization в списке Bearer-запросы из урока про JWT пройдут preflight и упадут уже там. ExposeHeaders открывает JS-у X-Total-Count, MaxAge кэширует preflight на полдня.

Проверка: кто именно блокирует

Фронт на localhost:5173 зовёт POST /api/v1/todos, на сервере CORS не настроен вовсе. Что произойдёт?

AllowOrigins, AllowAllOrigins и AllowOriginFunc#

Белый список AllowOrigins — рабочая норма: перечислите origin-а целиком, со схемой и портом, без завершающего слэша — http://localhost:5173, https://goma.dev; сравнение строковое, и «почти тот же origin» не пройдёт. Для публичного API без cookie и Authorization существует AllowAllOrigins: true — сервер отвечает Access-Control-Allow-Origin: и разрешает чтение всем; для открытых справочников погоды и курсов — честный выбор, для всего с аутентификацией — нет: браузер отклоняет сочетание «» с credentials — если Access-Control-Allow-Credentials: true, звёздочка в Allow-Origin игнорируется, и ответ скрыт; cookie-сессии из урока про cookie требуют точного origin-а в заголовке. Промежуточные случаи решают два инструмента. AllowOriginPatterns — список масок вида "https://*.goma.dev": поддомены продукта без ручного списка; звёздочка внутри маски работает на сегмент хоста, схемы и порты маска тоже учитывает. AllowOriginFunc — функция func(origin string) bool: верните true, чтобы разрешить; внутри — любые проверки, от regex до похода в таблицу арендаторов: для мультитенантных систем, где каждому клиенту выдан свой поддомен, это единственный путь. При динамических разрешениях правило кэширования из урока про статику приобретает зубы: разным origin-ам уходят разные заголовки, поэтому Vary: Origin обязателен (включайте его и сами, если рисуете заголовки руками), иначе CDN с радостью закэширует ответ для goma.dev и раздаст его тестовому стенду. И гигиена: не отражайте присланный Origin без проверки — «верну тебе твой Origin, что бы это ни было» эквивалентно звёздочке, только с притворством безопасности. Наконец, валидация конфига: cors.New прогоняет Config через Validate и паникует на противоречиях при старте — не настроили ни одного источника разрешений, сервер не поднимется; это раздражает один раз на деве и спасает прод, где нет CORS без списков.

Конфиг без origin-ов

Прочитайте код и выберите, что он выведет:

r.Use(cors.New(cors.Config{
        AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodOptions},
        AllowHeaders: []string{"Origin", "Content-Type"},
        MaxAge:       12 * time.Hour,
}))

Звено gin cors: где вешать и как не перепутать порядок#

CORS-звено гибко в масштабе, и от места вешания зависит, кто получит заголовки. r.Use(cors.New(...)) на движке — все маршруты, включая группы /api/v1 из урока про роуты и HTML-страницы из урока про шаблоны; для монолита с одним origin-ом фронтенда это нормально, но страницы CORS не нужны, и звено честно молчит для same-origin запросов: без заголовка Origin в запросе cors не ставит ничего. На группе — api := r.Group("/api/v1"); api.Use(cors.New(...)) — заголовки получит только API, а статика из урока 11 и HTML останутся без лишних заголовков; для нескольких фронтендов с разными правами собирают по звену на группу — админке один AllowOrigins, публичному API другой, и конфиги не пересекаются. Два правила порядка. Первое: cors выше Auth — preflight-запрос OPTIONS не несёт Authorization, и если гвард из урока про JWT стоит раньше, он оборвёт OPTIONS с 401 ещё до CORS-ответа; браузер покажет «preflight failed», а вы будете искать проблему в конфиге, который ни при чём. Второе: одно звено на маршрут — двойной r.Use(cors.New(...)) на движке и на группе даст два Access-Control-Allow-Origin в одном ответе, и браузер отклонит ответ как некорректный: дубликаты CORS-заголовков запрещены. Дальше — взаимодействие с другими звеньями: limitBody из урока про загрузку файлов на OPTIONS не мешает (тело пустое), но звено логирования из урока про recovery теперь пишет в лог и preflight-ы — при желании фильтруйте их, чтобы логи не распухали. И про 404: если cors висит на движке через r.Use, он работает и на несуществующих маршрутах — браузер увидит аккуратный 404 вместо «блокировки»; если звено на группе, промах мимо группы получит ответ без CORS-заголовков, и отладка будет странной. Универсальный совет: держите cors на самом верхнем уровне, где нужен, и проверяйте порядок звеньев до того, как фронтенд начнёт жаловаться.

Прод-конфиг: env, паттерны и AllowOriginFunc

GO
func corsConfig() cors.Config {
        origins := strings.Split(os.Getenv("CORS_ORIGINS"), ",") // список через запятую
        if len(origins) == 0 {
                panic("CORS_ORIGINS пуст: перечислите origin-ы через запятую")
        }
        return cors.Config{
                AllowOriginPatterns: origins, // https://*.goma.dev тоже работает
                AllowOriginFunc: func(origin string) bool {
                        // мультитенантность: tenant.goma.dev и сверки в базе
                        return strings.HasSuffix(origin, ".goma.dev") ||
                                slices.Contains(origins, origin)
                },
                AllowMethods: []string{
                        http.MethodGet, http.MethodPost,
                        http.MethodPut, http.MethodDelete,
                        http.MethodOptions,
                },
                AllowHeaders:     []string{"Origin", "Content-Type", "Authorization"},
                ExposeHeaders:    []string{"X-Total-Count"},
                AllowCredentials: true,
                MaxAge:           12 * time.Hour,
        }
}

r.Use(cors.New(corsConfig()))

Origin-ы живут в переменной окружения и не вшиты в бинарник — дев, стейджинг и прод получают свои списки без пересборки. AllowOriginPatterns покрывает поддомены маской, AllowOriginFunc решает нестандартные случаи: суффикс арендатора плюс явный список. Оба инструмента в одном конфиге работают: функция вызывается, когда строки и маски не совпали. Пустой CORS_ORIGINS — паника на старте, по аналогии с пустым JWT_SECRET из урока про JWT.

Безопасность gin за пределами CORS: звено заголовков#

CORS закрывает один вопрос — чтение cross-origin ответов, — но безопасность gin шире: браузер уважает ещё десяток заголовков, и большинство из них ставится одним звеном, механика которого — урок про кастомные middleware: c.Writer.Header().Set до c.Next(). Strict-Transport-Security: max-age=63072000; includeSubDomains — HSTS: получив его по HTTPS, браузер на два года забывает про http для этого домена и переводит все запросы на https сам; ставьте только там, где HTTPS уже факт, — на http-деве заголовок бессмыслен и только путает. X-Content-Type-Options: nosniff — запрет MIME-sniffing: без него браузер угадывает тип по содержимому и может «починить» text/plain до исполняемого HTML; с nosniff он уважает Content-Type и отвергает стили и скрипты с чужим типом. Связка с загрузками из урока про файлы прямая: пользовательский файл, отдающийся как text/html, — это хранимый XSS, и nosniff — второй рубеж после http.DetectContentType. X-Frame-Options: DENY (или SAMEORIGIN) — запрет встраивания в iframe, против clickjacking: страница-приманка поверх вашего сайта не соберётся. Content-Security-Policy — whitelist источников: default-src 'self' разрешает грузить скрипты, стили и картинки только со своего origin-а, инлайновые скрипты и внешние CDN молча блокируются; CSP — самый мощный противовес XSS после экранирования из урока про шаблоны, но и самый капризный — начинайте с report-only режима и ужесточайте по логам. Referrer-Policy: strict-origin-when-cross-origin — наружу уходит только origin, а не полный URL с токенами в query. И гигиена сигнатур: net/http подписывает ответы «Server: gin» — звену по силам Header().Del("Server"), чтобы не публиковать фреймворк и версию собирающим статистику сканерам. Это не чек-лист «поставь всё», а меню: выберите, что ваш продукт должен запретить, и вынесите в одно звено рядом с cors — две строки на r.Use, вся безопасность заголовков в одном месте.

secureHeaders: пять строк в одном звене

GO
func secureHeaders() gin.HandlerFunc {
        return func(c *gin.Context) {
                h := c.Writer.Header()
                h.Set("X-Content-Type-Options", "nosniff")
                h.Set("X-Frame-Options", "DENY")
                h.Set("Referrer-Policy", "strict-origin-when-cross-origin")
                if os.Getenv("APP_ENV") == "production" {
                        h.Set("Strict-Transport-Security",
                                "max-age=63072000; includeSubDomains")
                }
                h.Set("Content-Security-Policy", "default-src 'self'")
                h.Del("Server") // не подписываемся gin-ом
                c.Next()
        }
}

r.Use(secureHeaders())

Та же механика, что и у любого звена из урока про кастомные middleware: Set до c.Next(), и заголовки уезжают в каждом ответе. HSTS включён только в проде — на http-деве он мешает. CSP default-src 'self' — стартовая точка: шаблоны из урока про HTML продолжат работать, инлайновые скрипты — нет; ужесточайте через report-only.

Проверка: зачем nosniff

Загруженный пользователем файл раздаётся с Content-Type: text/plain, но внутри HTML со скриптом. Что сделает браузер с X-Content-Type-Options: nosniff?

CSRF-обзор: что CORS не решает#

CSRF — cross-site request forgery — атака, при которой страница злоумышленника отправляет запрос на ваш API от имени залогиненного пользователя, и браузер сам прикладывает cookie. Механика: вы залогинены в трекер, сессионная cookie живёт; в соседней вкладке открыт bad-site.example, который содержим форму <form action="http://localhost:8080/api/v1/todos" method="POST"> и скрипт, её отправляющий; браузер выполняет cross-site POST на ваш сервер и — если SameSite позволяет — прикладывает вашу cookie; сервер видит «свой» пользовательский cookie и создаёт задачу. Обратите внимание: CORS тут бессилен, и это самый частый misconception темы — CORS ограничивает чтение ответа браузером, но не отправку запроса; bad-site и не хотел читать ответ, ему хватило самого факта «сервер выполнил действие». Первая линия обороны уже стоит: SameSite из урока про cookie. Lax — дефолт современных браузеров — пропускает cross-site cookie только в переходах верхнего уровня по GET; форма POST с чужого сайта cookie не получит, и CSRF через форму умирает; Strict — отрезает и переходы; None — оставляет дыру и обязывает к следующей линии. Вторая линия — CSRF-токен: сервер при выдаче формы кладёт в сессию псевдослучайное значение и рисует его скрытым полем; обработчик сверяет пришедшее поле с сессией — чужой сайт значение не знает и подделать не может; пакет gorilla/csrf умеет это из коробки вместе с HMAC-защитой cookie-токена. Третья — проверка Origin/Referer в звене: если Origin есть и он не в белом списке — 403. JWT из прошлого урока сам по себе CSRF-устойчив: Authorization-заголовок браузер никуда не прикладывает автоматически — атака требует, чтобы JS жертвы сам положил токен, а на чужой странице он недоступен. Поэтому связка «JWT в заголовке для API, сессии с SameSite для SSR» — не только удобство, но и безопасность: у каждой схемы свой фронт. Это обзор — полный разбор с формами и токенами заслуживает отдельного урока, а сегодня зафиксируйте главное: SameSite по умолчанию, CSRF-токен на state-changing формах, и не путайте CORS с защитой от CSRF.

Проверка: CORS против CSRF

Какое утверждение о CORS и CSRF верное?

Справочник: заголовки CORS и безопасности#

ЗаголовокКто ставит и гдеЧто делает
Access-Control-Allow-Originсервер, cors-звенокакой origin может читать ответ; * или точное имя
Access-Control-Allow-Methodsсервер, ответ на preflightразрешённые методы cross-origin запроса
Access-Control-Allow-Headersсервер, ответ на preflightразрешённые заголовки запроса: Content-Type, Authorization
Access-Control-Allow-Credentialsсерверtrue — браузер приложит cookie и Authorization; не сочетается с *
Access-Control-Expose-Headersсерверкакие заголовки ответа видит JS: X-Total-Count
Access-Control-Max-Ageсервер, ответ на preflightсколько секунд браузер кэширует preflight
Vary: Originсервер, cors-звенокэш не смешивает ответы разных origin-ов
Strict-Transport-Securityзвено secureHeadersбраузер принудительно переводит домен на HTTPS
X-Content-Type-Options: nosniffзвено secureHeadersзапрет MIME-sniffing на раздаче
X-Frame-Options: DENYзвено secureHeadersзапрет встраивания страницы в iframe
Content-Security-Policyзвено secureHeaderswhitelist источников скриптов, стилей, картинок
Referrer-Policyзвено secureHeadersчто уходит в Referer на внешние переходы

Отладка CORS: curl вместо браузера#

CORS-проблемы решаются curl-ом за минуты, потому что preflight — обычный HTTP-запрос: curl -i -X OPTIONS http://localhost:8080/api/v1/todos -H "Origin: http://localhost:5173" -H "Access-Control-Request-Method: POST" -H "Access-Control-Request-Headers: Content-Type,Authorization" — и вы смотрите на ответ как на обычный: 204 и четыре заголовка означают «preflight пройдёт», 401 говорит «поймал Auth-гвард — поднимите cors выше звеньев», 404 — звено не на том уровне, а молчаливое 204 без Access-Control-Allow-Origin — origin не в списке или конфиг не применился. Дальше настоящий запрос: curl -i -X POST http://localhost:8080/api/v1/todos -H "Origin: http://localhost:5173" -H "Content-Type: application/json" -d '{"title":"тест"}' — в ответе должен быть Access-Control-Allow-Origin, иначе JS ответ не увидит, даже если всё остальное идеально. Четыре частые грабли. Origin обязан совпасть до символа: схема, хост и порт, без завершающего слэша; http://localhost и http://localhost:5173 — разные origin-ы, и порт в http обязателен. Забытый AllowHeaders: Content-Type прислали, в списке его нет — preflight вернётся с пустым Allow-Headers, и браузер откажется слать настоящий запрос; сообщение в консоли при этом жалуется на заголовок, а не на конфиг. Дубликат звена: cors на движке и на группе — два Access-Control-Allow-Origin, браузер отбраковывает ответ. Пятиминутный кэш: правите конфиг, проверяете — а браузер всё ещё держит старый preflight из кэша Max-Age; в DevTools поставьте галку Disable cache или откройте инкогнито. И финальный аккорд: браузерная консоль при CORS-ошибке пишет причину прямо в тексте — «No 'Access-Control-Allow-Origin' header is present», «Response to preflight request... status 401» — читайте её до того, как лезть в код.

Соберите CORS-конфиг

Допишите звено: создать middleware cors, разрешить origin фронтенда и нужные заголовки, включить cookie и кэш preflight.

r.Use(cors.(cors.Config{ : []string{"http://localhost:5173"}, AllowMethods: []string{ http.MethodGet, http.MethodPost, http.MethodOptions, }, : []string{"Origin", "Content-Type", "Authorization"}, AllowCredentials: , : 12 * time.Hour, }))

Соберите путь preflight-запроса

Фронт на 5173 зовёт POST /api/v1/todos с JSON и Bearer-токеном: разложите события от fetch до 201.

Строки

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

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

Карточки: CORS и безопасность

Термин

Same-origin policy

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

Origin — схема плюс хост плюс порт. Браузер не даёт JS читать ответы чужого origin-а; запрос выполняется, сервер отрабатывает, но ответ скрыт без согласия сервера. curl, Postman и сервер-сервер — без CORS: правило только браузера.

1 / 6

Семь правил CORS и безопасности#

  • CORS — правило браузера о чтении: curl работает всегда, а защита API — токены и звенья, не cors-конфиг.
  • Origin совпадает до символа: схема, хост, порт, без слэша; http://localhost и http://localhost:5173 — разные origin-ы.
  • Preflight ловит всё не-simple: JSON-заголовок, Authorization и PUT/DELETE; если падает 401 — cors стоит ниже гварда, поднимите его выше.
  • AllowCredentials не сочетается со звёздочкой: для cookie и Authorization перечисляйте origin-ы явно; пустой список — cors.New паникует на старте.
  • Vary: Origin при динамических заголовках — иначе CDN закэширует ответ одного origin-а для всех; ExposeHeaders открывает JS-у X-Total-Count.
  • Безопасность заголовков — одно звено: HSTS в проде, nosniff, X-Frame-Options, CSP с report-only, Referrer-Policy; два r.Use — заголовков по две копии и браузер их отбракует.
  • CSRF — не про CORS: SameSite — первая линия, CSRF-токен — вторая, проверка Origin — третья; JWT-заголовок устойчив сам по себе, а сессионный SSR требует токена.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Same-origin policy запрещает JS читать ответы чужого origin-а; CORS — согласие сервера заголовками, и только браузер его уважает: curl работает всегда.
  • Preflight OPTIONS предшествует не-simple запросам — JSON, Authorization, PUT/DELETE; ответ 204 с Allow-Origin, Allow-Methods, Allow-Headers и Max-Age, затем настоящий запрос со своими заголовками.
  • gin cors — звено cors.New(cors.Config{...}): AllowOrigins точным списком, AllowAllOrigins для публичных API, AllowOriginPatterns и AllowOriginFunc для поддоменов и арендаторов; пустой конфиг — паника на старте.
  • AllowCredentials не сочетается со звёздочкой: cookie-сессии требуют точного origin-а; ExposeHeaders открывает JS-у X-Total-Count; Vary: Origin оберегает кэши.
  • Порядок звеньев: cors выше Auth, иначе preflight ловит 401; одно звено на маршрут — дубликаты Access-Control-Allow-Origin браузер отбраковывает.
  • Заголовки безопасности собираются в одно звено: HSTS только в проде, nosniff против MIME-sniffing, X-Frame-Options против clickjacking, CSP как вторая линия после экранирования шаблонов, Referrer-Policy.
  • CORS не защищает от CSRF: SameSite — дефолтная линия, CSRF-токен на state-changing формах, проверка Origin; JWT в Authorization-заголовке CSRF-устойчив сам по себе.

Домашнее задание

Настройте CORS для трекера с раздельным фронтендом: звено cors.New с AllowOrigins из CORS_ORIGINS, AllowMethods на CRUD, AllowHeaders с Content-Type и Authorization, ExposeHeaders с X-Total-Count, AllowCredentials: true и MaxAge 12 часов; пустая переменная окружения роняет старт с внятной ошибкой. Поставьте secureHeaders с nosniff, X-Frame-Options, Referrer-Policy и HSTS только в проде, повесьте cors выше Auth-гварда из прошлого урока и убедитесь curl-ом, что OPTIONS на /api/v1/todos отвечает 204 с четырьмя заголовками без токена. Проверьте негативные сценарии: чужой Origin, дубль cors-звена и забытый AllowHeaders — запишите, что видит браузер в каждом случае. Разберите форму POST на сессиях из урока про cookie и решите, нужна ли ей CSRF-защита при SameSite=Lax, — ответ запишите одним абзацем.

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

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

Почему curl работает, а браузер пишет «blocked by CORS policy»?

CORS — правило браузера: он шлёт запрос, но скрывает ответ без Access-Control-Allow-Origin. curl и Postman — не браузеры, правила не соблюдают и заголовки не проверяют. Сервер при этом отрабатывает оба случая одинаково.

OPTIONS на мой API отвечает 401. Что не так?

Preflight не несёт Authorization, и гвард стоит выше cors-звена. Перевесьте cors выше Auth (на движке или на группе до Use(Auth)) — OPTIONS будет отвечать 204 из cors-звена, не доходя до гварда.

Можно ли включить AllowAllOrigins с AllowCredentials?

Нет: браузер игнорирует Access-Control-Allow-Origin: * при credentials — ответ останется скрытым. Перечисляйте origin-ы явно или используйте AllowOriginPatterns и AllowOriginFunc, чтобы выписывать согласие только доверенным origin-ам.

CORS настраивается — и я защищён от CSRF?

Нет: CORS ограничивает чтение ответа, но не отправку запроса — чужой сайт отправит форму с cookie пользователя, и сервер выполнит действие. От CSRF защищают SameSite=Lax/Strict, CSRF-токены (gorilla/csrf) и проверка Origin в звене.

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

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

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

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

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

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

Echo: минималистичный быстрый фреймворк

Курс Echo для Go: маршруты, middleware, валидация, JWT, SSE, тестирование и деплой. 25 уроков с примерами и итоговым API-проектом.

  • Строить REST API на Echo
  • Использовать типизированные запросы/ответы (Request/Response)
  • Управлять ошибками через echo.HTTPError
  • Писать middleware, включая JWT-аутентификацию