Содержание урока · 15 разделов
- fiber cookie: память, которой у HTTP нет
- Сверка с res.go: одиннадцать полей fiber.Cookie
- Чтение и похоронка: c.Cookies и ClearCookie
- Атрибуты безопасности: HTTPOnly, Secure, SameSite
- Cookie против сессии: где чьи данные
- Cookie и сессия: кто где живёт
- session middleware v3: два пути и FromContext
- session.Config v3.5.0: поля и дефолты
- Жизненный цикл: Save, Destroy, Regenerate, Release
- Хранилища: fiber.Storage и адаптеры
- Ловушки кук и сессий: событие, симптом, лечение
- Запуск, шесть сценариев curl и что дальше
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
fiber cookie: память, которой у HTTP нет#
HTTP — протокол без состояния: каждый запрос для сервера рождён заново, никто не помнит, что пять минут назад этот же браузер логинился, складывал товары в корзину или выбрал тёмную тему. Механизм чинит это на уровне заголовков. Сервер в ответе отдаёт строку Set-Cookie: имя=значение плюс атрибуты срока и безопасности; браузер сохраняет куку и с каждым следующим запросом к этому домену возвращает её в заголовке Cookie. Память живёт не на сервере и не в протоколе, а у клиента — и это определяет всё: и возможности, и ловушки сегодняшней темы «fiber cookie».
Fiber здесь работает вплотную к fasthttp: чтение кук — это разбор заголовка Cookie запроса, запись — добавление Set-Cookie в ответ. Но над сырыми заголовками поднят типизированный слой: структура fiber.Cookie с одиннадцатью полями и три метода на контексте — запись, чтение и удаление. Мы пройдём по исходникам тега v3.5.0 в порядке жизни запроса: сначала поставим куку, потом прочитаем, потом погасим — и только затем поднимемся на уровень выше, к сессиям, где в куке лежит лишь идентификатор, а настоящие данные — в серверном хранилище.
Сверка с res.go: одиннадцать полей fiber.Cookie#
Сверимся с первоисточником. В корневом ctx.go тега v3.5.0 слова Cookie нет вообще — метод записи живёт в res.go на DefaultRes и документирован дословно: Cookie sets a cookie by passing a cookie struct. Ctx подмешивает его через встраивание, поэтому в хендлере пишут просто c.Cookie(&fiber.Cookie{...}). Сама структура описана в res.go рядом с методом и несёт одиннадцать полей: Expires time.Time, Name string, Value string, Path string, Domain string, SameSite string, MaxAge int, Secure bool, HTTPOnly bool, Partitioned bool и SessionOnly bool. Никаких вложенных типов: срок — время, возраст — секунды, остальное — строки и флаги.
Метод делает незаметную, но важную работу — нормализацию, и комментарий к нему прямо предупреждает: The argument is treated as read-only: the normalization this method applies (default Path, SessionOnly, and the Secure implied by SameSite=None or Partitioned) happens on a local copy, so a caller may reuse the same *Cookie template across requests. То есть: пустой Path становится корнем "/"; SessionOnly обнуляет и MaxAge, и Expires — получается кука без срока, живущая до закрытия браузера; SameSite=None тянет за собой Secure=true по требованию RFC; то же с Partitioned. А если SameSite не заполнен вовсе, сработает ветка по умолчанию — Lax. Константы на любой вкус объявлены в constants.go: fiber.CookieSameSiteLaxMode = "Lax", CookieSameSiteStrictMode = "Strict", CookieSameSiteNoneMode = "None" и CookieSameSiteDisabled = "disabled" — последний не из RFC, это способ вообще не писать атрибут. И финальная деталь: перед отправкой куку проверяет hc.Valid() из net/http — комментарий честен: invalid cookies are ignored, same approach as net/http; испорченная кука молча не уйдёт, ошибки хендлер не увидит.
Трио методов: c.Cookie, c.Cookies, c.ClearCookie
GO// POST /api/prefs — поставить куку темы
api.Post("/prefs", func(c fiber.Ctx) error {
// запись: собираем структуру fiber.Cookie целиком
c.Cookie(&fiber.Cookie{
Name: "theme",
Value: "dark",
Path: "/api", // пустой Path сам станет "/"
MaxAge: 7 * 24 * 3600, // семь дней, в секундах
HTTPOnly: true, // JS эту куку не увидит
Secure: true, // только по HTTPS
SameSite: "Lax", // или fiber.CookieSameSiteLaxMode
})
return c.SendStatus(fiber.StatusOK)
})
// GET /api/prefs — чтение: куки нет — вернётся default
api.Get("/prefs", func(c fiber.Ctx) error {
theme := c.Cookies("theme", "light") // "light" вместо пустой строки
return c.JSON(fiber.Map{"theme": theme})
})
// DELETE /api/prefs — удаление: кука-похоронка
api.Delete("/prefs", func(c fiber.Ctx) error {
c.ClearCookie("theme") // без аргументов погасит ВСЕ куки запроса
return c.SendStatus(fiber.StatusNoContent)
})Чтение и запись в v3 разведены по разным методам, и разница в одну букву — источник опечаток: запись — c.Cookie со структурой (Cookie sets a cookie by passing a cookie struct), чтение — c.Cookies строкой. В v2 имя c.Cookie занимало чтение, поэтому чужие сниппеты стоит проверять на версию. Аргумент записи — read-only: нормализация (Path, SessionOnly, Secure при None) происходит на локальной копии, один шаблон можно переиспользовать на многих запросах. Испорченная кука не вернёт ошибку — она просто не уйдёт: invalid cookies are ignored, same approach as net/http.
Проверьте себя: SameSite=None тянет Secure
В хендлере одна строка: c.Cookie(&fiber.Cookie{Name: "t", Value: "1", SameSite: "None"}). Поле Secure не заполняли. Что уйдёт в заголовке Set-Cookie?
Чтение и похоронка: c.Cookies и ClearCookie#
Чтение живёт в req.go — сигнатура c.Cookies(key string, defaultValue ...string) string, и док-комментарий договаривает контракт: Cookies are used for getting a cookie value by key. Defaults to the empty string "" if the cookie doesn't exist. If a default value is given, it will return that value if the cookie doesn't exist. То есть отсутствие куки — не ошибка и не паника, а пустая строка или ваш default вторым аргументом; под капотом — fasthttp Request.Header.Cookie(key), разбор заголовка Cookie запроса. Проверять ok, как у карт, не нужно: в Go-строке пустота и отсутствие неразличимы, и именно поэтому есть defaultValue.
Удаление — c.ClearCookie(key ...string), тоже из res.go: ClearCookie expires a specific cookie by key on the client side. If no key is provided it expires all cookies that came with the request. Слово expires точнее, чем «удаление»: сервер не может стереть куку у клиента — он может лишь попросить браузер считать её истёкшей. Внутри это fasthttp DelClientCookie: уходит кука с погасшим сроком, и браузер выкидывает старую. Без аргументов ClearCookie гасит все куки, пришедшие с запросом, — удобный «выход из всего», но и способ случайно потерять безобидные настройки, поэтому в коде его зовут редко и осознанно.
Атрибуты безопасности: HTTPOnly, Secure, SameSite#
Три атрибута превращают куку из инструмента удобства в инструмент доверия. HTTPOnly отрезает куку от JavaScript: document.cookie её не вернёт, укравший страницу скрипт не увидит значение — но сама кука по-прежнему прикладывается к каждому запросу, серверу она видна как прежде. Secure запрещает отправку по открытому HTTP: только TLS. SameSite решает, уедет ли кука в cross-site запрос — тот случай, когда чужой сайт дёргает ваш API из браузера жертвы. Lax — режим по умолчанию в v3 и в fiber.Cookie, и в session.Config: кука уходит с верхнеуровневых безопасных переходов (клик по ссылке), но не с cross-site POST — большая часть CSRF-атак на этом обрубается. Strict — даже по ссылкам не уедет: самый жёсткий режим ценой неудобства после перехода с чужого сайта. None — кука едет всегда, но только с Secure.
Свяжем с восьмым уроком: CORS и SameSite закрывают два разных плеча одной дыры. CORS регулирует, кто имеет право ПРОЧИТАТЬ ответ вашего API из чужого скрипта; SameSite — уйдёт ли кука браузера с cross-site запроса вообще. Один без другого дыряв: разрешённый CORS без SameSite даёт чужому сайту слать запросы с куками, Strict SameSite без CORS-дисциплины — тупик для легального SPA. Для форм в v3.5.0 есть и профильный инструмент — middleware/csrf, у которого в конфиге поле Session *session.Store: состояние CSRF-токена можно держать прямо в session store, официальная стыковка сегодняшней темы с защитой форм. А если в куках появляются нечувствительные, но приватные значения — middleware/encryptcookie шифрует их AES-GCM: ключ обязателен (16, 24 или 32 байта после base64-декодирования), GenerateKey умеет его создавать, Except перечисляет куки, которые шифровать не нужно.
Проверьте себя: что видит скрипт
Кука session_id поставлена с атрибутами HTTPOnly: true и Secure: true. XSS-скрипт на вашей странице выполняет document.cookie. Что он получит?
Cookie против сессии: где чьи данные#
Теперь второй слой — fiber сессии. Идея проста: кука плоха как хранилище данных. Она строковая, у неё жёсткий размер (браузеры держат порядка четырёх килобайт на куку), она уезжает на клиент и там читается и правится руками — в devtools любую куку видно и можно подделать, а HTTPS и флаги защищают канал, но не сам файл кук на чужой машине. Сессия переворачивает схему: в куке живёт только случайный идентификатор session_id, а данные — gob-кодированная карта ключ-значение — лежит в Storage на сервере. Клиент может подделать id? Может попробовать, но валидный id — 256-битный токен из KeyGenerator, угадать его нельзя, а чужой id либо не найдётся в хранилище, либо будет разобран как чужая сессия. Клиент может прочитать id? Да, но id — не секрет, а адрес: полезность имеет лишь в паре с хранилищем.
Отсюда правило распределения: «кто я» и всё, что даёт права, — в сессию; безобидная косметика вроде свёрнутой панели — можно и в куку. Любое значение куки — это слово клиента, и строить на нём авторизацию нельзя; любое значение сессии — слово сервера самому себе. Таблица ниже фиксирует разницу, а дальше идём в исходники middleware/session — там эта схема реализована буквально.
Cookie и сессия: кто где живёт#
| Свойство | Cookie | Сессия |
|---|---|---|
| Где данные | у клиента, в файле кук браузера | на сервере, в Storage за id из куки session_id |
| Что видит пользователь | всё: имя, значение, атрибуты — и правит в devtools | только случайный id; данные ему недоступны |
| Размер | порядка 4 КиБ на куку, только строки | объём хранилища; gob-кодирует и сложные типы |
| Срок жизни | MaxAge или Expires, SessionOnly — до закрытия браузера | IdleTimeout простоя и опциональный AbsoluteTimeout |
| Доверие сервера | никакое: значение — слово клиента | полное: данные писал сам сервер |
| Типовое содержимое | тема, флаг «баннер закрыт», remember-токен | логин, роли, корзина, flash-сообщения |
session middleware v3: два пути и FromContext#
Пакет github.com/gofiber/fiber/v3/middleware/session в v3 даёт два пути работы, и выбор определяет контракт сохранения. Путь первый — middleware: session.New() возвращает fiber.Handler, который вешается через app.Use. Внутри он сам загружает сессию до вашего хендлера и сам сохраняет её после — в хендлере остаётся только session.FromContext(c), который вернёт session.Middleware с методами Set, Get, Delete, Destroy, Regenerate, Fresh и ID. Важная мелочь из исходников: session.New(config) с конфигом discard-ит store — наружу отдаётся только хендлер; если store нужен вам отдельно (например, для GetByID из фоновых задач), берите второй путь — session.NewWithStore(config...) из middleware/session/store.go, возвращающий пару (fiber.Handler, Store). Смешивать нельзя: если middleware уже загрузил сессию, вызов store.Get(c) вернёт ошибку ErrSessionAlreadyLoadedByMiddleware — «сессия уже загружена, второй раз не выдаём».
Выше уже названы дефолты, и все они собраны в middleware/session/config.go, где ConfigDefault выглядит так: IdleTimeout 30 минут, KeyGenerator utils.SecureToken, Extractor extractors.FromCookie("session_id"), CookieSameSite "Lax"; полный список полей с комментариями исходника — в перечне ниже.
Куку middleware ставит сам. В session.go метод setSession пишет куку с ключом из Extractor (по умолчанию session_id), значением — id сессии, путём и доменом из конфига; если CookieSessionOnly не взведён, добавляются MaxAge и Expires на idleTimeout вперёд. Атрибуты безопасности берутся из конфига: SameSite с дефолтом Lax (None — и Secure добавится сам, как и в c.Cookie), Secure и HTTPOnly — флаги из CookieSecure и CookieHTTPOnly. И одно предупреждение из комментария Session: структуры request-scoped — после конца запроса сессию использовать нельзя, а Release возвращает её в пул.
session.Config v3.5.0: поля и дефолты#
- Storage — fiber.Storage, Optional. Default: memory.New() — куда складывать данные сессий
- Store — *session.Store, Required: NewWithStore заполнит сам, вручную не трогают
- Next — func(fiber.Ctx) bool, Optional: вернуть true — middleware пропустит запрос
- ErrorHandler — Optional: дефолт пишет «session error» в лог и отдаёт 500 Internal Server Error
- KeyGenerator — func() string, Optional. Default: utils.SecureToken — генератор id сессий
- CookieDomain и CookiePath — Optional, по умолчанию пусто
- CookieSameSite — Optional. Default: "Lax"
- Extractor — Optional. Default: extractors.FromCookie("session_id") — где искать id запроса
- IdleTimeout — Optional. Default: 30 * time.Minute; обновляется на каждом Save
- AbsoluteTimeout — Optional, 0 = нет; меньше IdleTimeout — паника при старте
- CookieSecure — Optional. Default: false
- CookieHTTPOnly — Optional. Default: false
- CookieSessionOnly — Optional. Default: false; кука умрёт с браузером, но данные в store останутся
Вход: POST /api/auth/login через Bind().Form
GO// LoginForm — форма входа: тот же форм-биндер, что в одиннадцатом уроке
type LoginForm struct {
Username string `form:"username" validate:"required,min=3"`
Password string `form:"password" validate:"required"`
Remember string `form:"remember"` // чекбокс: "on" или пусто
}
func main() {
app := fiber.New()
// слои из шестого-восьмого уроков
app.Use(helmet.New())
app.Use(cors.New())
app.Use(logger.New())
// сессии: NewWithStore вернёт и хендлер, и store
sessHandler, store := session.NewWithStore(session.Config{
IdleTimeout: 30 * time.Minute, // дефолт, показан явно
CookieHTTPOnly: true, // id сессии не виден JS
CookieSameSite: "Lax", // дефолт, повторим явно
})
// middleware раньше группы /api: страж ниже уже увидит сессию
app.Use(sessHandler)
api.Post("/auth/login", func(c fiber.Ctx) error {
var in LoginForm
// форма входа разбирается тем же Bind().Form
if err := c.Bind().Form(&in); err != nil {
return err
}
// проверка пароля — учебная заглушка
if in.Password != "s3cret" {
return fiber.NewError(fiber.StatusUnauthorized,
"неверная пара логин-пароль")
}
sess := session.FromContext(c)
// новый id после входа: старый мог быть подложен
if err := sess.Regenerate(); err != nil {
return err
}
sess.Set("user", in.Username)
if in.Remember == "on" {
// remember-me: долгая кука, но только для /api
c.Cookie(&fiber.Cookie{
Name: "remember",
Value: in.Username, // в бою — подписанный токен
Path: "/api",
MaxAge: 30 * 24 * 3600,
HTTPOnly: true,
Secure: true,
SameSite: "Lax",
})
}
return c.JSON(fiber.Map{"user": in.Username})
})
log.Fatal(app.Listen(":8080"))
}Каркас канона: слои из шестого-восьмого уроков, затем session middleware, затем маршруты — порядок не обсуждается, страж группы /api обязан видеть сессию в Locals, а FromContext внутри стража вернёт nil, если middleware не отработал. Regenerate перед Set — правило гигиены: session.Regenerate generates a new session id and deletes the old one from storage, и старый id, который мог подложить злоумышленник, умирает вместе со своим содержимым. Remember-кука хранит в учебнике имя, в бою — подписанный токен из базы: значение куки всегда слово клиента, доверять ему можно только после сверки. Обратите внимание на Save: его здесь нет нигде — и это не забывчивость, а контракт middleware, разбор ниже.
Проверьте себя: обязательный ли Save
Приложение собрано с app.Use(session.New()). Хендлер вызвал sess.Set("user", "anna") и НЕ вызвал Save. Что окажется в хранилище после ответа?
Жизненный цикл: Save, Destroy, Regenerate, Release#
При ручном пути — store.Get(c) без middleware — контракт меняется на противоположный: сохранять обязаны вы. Save кодирует данные gob-энкодером (ошибка оборачивается как failed to encode session data: %w), пишет в Storage.SetWithContext и обновляет куку setSession-ом. Кастомные типы в данных требуют gob.Register — у Store есть RegisterType(i any) ровно для этого. Закончили — зовите sess.Release(): The session should not be used after calling this function, сессия возвращается в пул; при middleware-пути Release не нужен и даже вреден — releaseSession middleware вызывает сам.
Уничтожение — метод Destroy, и док-строка на него точна: Destroy deletes the session from storage and expires the session cookie. Внутри DestroyWithContext: Storage.DeleteWithContext(id), затем data.Reset() и delSession — куке ставятся MaxAge=-1 и Expires в прошлом, из заголовков запроса и ответа она тоже вычищается. Смена личности — Regenerate: новый id через KeyGenerator, старый удаляется из хранилища; Reset делает то же плюс обнуляет данные. Ещё два быстрых метода: Fresh() отвечает, свежая ли сессия (id только что сгенерирован), SetIdleTimeout(d) переносит срок — применится на следующем Save. И последняя деталь из исходников, о которой редко пишут: extractor, который принёс id, помнится в контексте, и если id пришёл из read-only источника — query, form, param, — для существующей сессии он считается attacker-controlled и в куку не продвигается; комментарий в session.go называет это защитой от session fixation.
Страж с двумя ключами: /api/auth/me и выход
GO// requireAPIKeyOrSession — страж седьмого урока, прокачанный до браузера
func requireAPIKeyOrSession(valid string) fiber.Handler {
return func(c fiber.Ctx) error {
// путь первый: API-клиент с заголовком из седьмого урока
if c.Get("X-API-Key") == valid {
return c.Next()
}
// путь второй: браузер с живой сессией после входа
if sess := session.FromContext(c); sess != nil &&
sess.Get("user") != nil {
c.Locals("user", sess.Get("user"))
return c.Next()
}
// путь третий: remember-me при сгоревшей сессии
if token := c.Cookies("remember"); token != "" {
// в бою — сверка токена с базой, здесь упрощённо
c.Locals("user", token)
return c.Next()
}
return fiber.NewError(fiber.StatusUnauthorized,
"ни ключа, ни сессии, ни remember-куки")
}
}
// группа /api теперь строится с новым стражем
api := app.Group("/api", requireAPIKeyOrSession("s3cret"))
api.Get("/auth/me", func(c fiber.Ctx) error {
// Locals положил страж: хендлеру разбираться не нужно
return c.JSON(fiber.Map{"user": c.Locals("user")})
})
api.Post("/auth/logout", func(c fiber.Ctx) error {
sess := session.FromContext(c)
// Destroy: данные из Storage, кука session_id — в прошлое
if err := sess.Destroy(); err != nil {
return err
}
// remember-куку гасим вручную: сервер не может стереть её сам
c.ClearCookie("remember")
return c.SendStatus(fiber.StatusNoContent)
})Это тот самый remember-me для requireAPIKey-инстанса: заголовок X-API-Key остаётся старшим ключом для API-клиентов, сессия и remember-кука добавляют браузерный путь, и все три ведут в один Locals("user") — хендлеры не знают, кем их пустили. Логика не симметрична: сессия доверенная (её писал сервер), remember-кука — слово клиента, в бою обязательна сверка токена с базой. Destroy гасит session_id, но чужую куку remember он не знает — её гасит ClearCookie: тот самый факт из середины урока, что сервер куки не стирает, а просит браузер считать истёкшей.
Атрибут Path решает, на какие пути браузер вообще отправляет куку. Кука remember с Path=/api уезжает только на маршруты, начинающиеся с /api, — статика из пятого урока её никогда не увидит. Это и защита (кука авторизации не мелькает в запросах к публичным файлам), и ловушка: страж на /api не увидит куку, поставленную с Path=/, — нет, увидит, наоборот — а вот кука с Path=/api/auth не дотянет до /api/tasks. Правило: сужайте Path до префикса, где кука реально нужна, и держите в уме, что Path=/api покрывает и /api/auth, и /api/tasks сразу.
Забытый Save: что увидит второй запрос?
Прочитайте код и выберите, что он выведет:
// Ручной путь: middleware в цепочке НЕТ, store.Get руками
store := session.NewStore()
api.Post("/cart", func(c fiber.Ctx) error {
sess, err := store.Get(c)
if err != nil {
return err
}
defer sess.Release()
// прибавляем товар к счётчику корзины
n, _ := sess.Get("items").(int)
sess.Set("items", n+1)
// Save() НЕ вызван — middleware не сохранит за нас
return c.JSON(fiber.Map{"items": n + 1})
})
// Первый запрос: curl -d "" localhost:8080/api/cart
// Второй запрос: curl -d "" localhost:8080/api/cartХранилища: fiber.Storage и адаптеры#
Session.Config.Storage принимает интерфейс fiber.Storage из storage_interface.go — девять методов, сгруппированных в пары с контекстом и без: Get и GetWithContext, Set и SetWithContext, Delete и DeleteWithContext, Reset и ResetWithContext, плюс Close. Контракт док-комментариев стоит выучить дословно: GetWithContext gets the value for the given key with a context — и nil, nil is returned when the key does not exist: отсутствие ключа — не ошибка, а пара нилей, именно на неё опирается getSession, решая, что сессию пора создавать свежей. Запись: SetWithContext stores the given value for the given key with an expiration value, 0 means no expiration — ноль означает «без срока», а пустые ключ или значение молча игнорируются, без ошибки.
В конфиге сессий Storage описан как Optional. Default: memory.New() — встроенная память. Честная деталь из сверки: она живёт в internal/storage/memory — пакет internal, из вашего модуля его не импортировать; комментарий исходника признаётся, что это копия внешнего пакета «для тестов». В своём коде берите модуль github.com/gofiber/storage — там Memory и десятки адаптеров: Redis, Memcache, SQLite3, Postgres, MySQL, MongoDB и другие; у каждого тот же интерфейс Storage, поэтому сессии переезжают между хранилищами заменой одной строки в конфиге. Встроенная память держит карту Entry с expiry uint32 и гоняет горутину сборщика мусора с интервалом GCInterval — для разработки идеально, для продакшена означает потерю всех сессий на рестарте: переживают перезапуск только внешние адаптеры.
Кука без срока: сколько проживёт?
Прочитайте код и выберите, что он выведет:
// SessionOnly не тронут, MaxAge не задан, Expires нулевой
c.Cookie(&fiber.Cookie{
Name: "intro",
Value: "shown",
})Flash-сообщения: паттерн через сессию
GO// Флеш — сообщение, которое переживёт ровно один redirect.
// Отдельного flash-middleware в v3.5.0 НЕТ — собираем из сессии.
// POST /api/tasks/form: создание задачи из формы (урок 11)
api.Post("/tasks/form", func(c fiber.Ctx) error {
var in TaskForm
if err := c.Bind().Form(&in); err != nil {
// кладём флеш и уводим обратно на форму
sess := session.FromContext(c)
sess.Set("flash_error", "проверьте поля формы")
return c.Redirect("/form")
}
nextID++
tasks = append(tasks, Task{ID: nextID, Title: in.Title,
CreatedAt: time.Now().Format(time.RFC3339)})
sess := session.FromContext(c)
sess.Set("flash_ok", "задача создана")
return c.Redirect("/form")
})
// GET /form: страница читает флеш и немедленно съедает его
app.Get("/form", func(c fiber.Ctx) error {
sess := session.FromContext(c)
msg, _ := sess.Get("flash_ok").(string)
errMsg, _ := sess.Get("flash_error").(string)
// прочитал — удалил: следующий запрос флеша уже не увидит
sess.Delete("flash_ok")
sess.Delete("flash_error")
return c.JSON(fiber.Map{"ok": msg, "error": errMsg})
})Flash — классическая схема POST-redirect-GET из CGI-эпохи: после POST браузер должен получить редирект, иначе F5 повторит отправку формы. Сообщение о результате кладётся в сессию — единственное место, пережидающее redirect, — и удаляется сразу после первого чтения. Middleware для этого в v3.5.0 нет, и паттерн выше — официальная замена: Set перед redirect, Get плюс Delete на следующем запросе. Сохранение происходит само — middleware в цепочке помнит контракт автосейва из квиза. Ключи-флеши продумывайте заранее: одна пара на успех, одна на ошибку — и Delete обязателен, иначе сообщение будет мигать на каждом обновлении страницы.
Допишите remember-me и выход
Вставьте пропуски: получение store, создание middleware, доступ к сессии в контексте, уничтожение и работа с кукой remember.
Соберите путь запроса с сессией
Расставьте шаги запроса на вход от команды в терминале до Set-Cookie в ответе.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Ловушки кук и сессий: событие, симптом, лечение#
| Событие | Что увидите | Лечение |
|---|---|---|
| SameSite: "None" без Secure | Fiber добавит Secure сам — кука едет в cross-site запросы | держать Lax/Strict; None — только для честно кросс-доменных кук |
| store.Get после session.New в цепочке | ErrSessionAlreadyLoadedByMiddleware от store.Get | один путь на приложение: либо middleware + FromContext, либо store.Get |
| Save забыт при ручном store.Get | кука и запись в Storage не уходят, состояние теряется тихо | .sess.Save() перед выходом из хендлера, defer sess.Release() |
| AbsoluteTimeout меньше IdleTimeout | panic при старте: [session] AbsoluteTimeout must be greater than or equal to IdleTimeout | задавать AbsoluteTimeout >= IdleTimeout или оставить 0 |
| кука без MaxAge и Expires | сессионная кука: умерла с закрытием браузера | указывать срок явно; SessionOnly — только как осознанный выбор |
| IdleTimeout в CookieSessionOnly-куке | данные в store живут idleTimeout, кука умерла с браузером | помнить комментарий конфига: CookieSessionOnly не удаляет данные из store |
Словарь урока
Запуск, шесть сценариев curl и что дальше#
Соберите каркас: слои helmet, cors, logger из шестого-восьмого уроков, session middleware из этого, группа /api со стражем requireAPIKeyOrSession, маршруты входа, выхода и /api/auth/me. Прогоните шесть сценариев. Первый: вход, curl -c jar.txt -d "username=anna&password=s3cret&remember=on" localhost:8080/api/auth/login — ответ 200 с {"user":"anna"}, а в jar.txt после -v вы найдёте две куки: session_id с Max-Age на idleTimeout и remember на тридцать дней. Второй: curl -b jar.txt localhost:8080/api/auth/me — страж пустил по сессии, Locals("user") вернулся. Третий: тот же запрос из чистого терминала без jar — 401: ни ключа, ни сессии, ни remember-куки. Четвёртый: curl -b jar.txt -X POST .../api/auth/logout — сессия уничтожена, кука гашена; повторный /api/auth/me с jar даёт 401. Пятый: кука с SameSite:"None" — в выводе curl -vSecure появился сам. Шестой: флеш — POST формы, redirect, GET /form с сообщением, повторный GET — уже пусто.
Материал держится на трёх опорах: страж и Locals — из седьмого урока, разграничение CORS и SameSite — из восьмого, форма входа и Bind().Form — из одиннадцатого; полный план — на странице курса Fiber. Песочница сайта сетевые сценарии не запускает — runner-ов в уроках Fiber нет, интерактивы здесь quiz, predict, fill, order и флешкарты; каркас собирайте локально в терминале.
Дальше — тринадцатый урок «HTML-шаблоны»: шаблонизаторы, html/template и рендер страниц. Сегодняшние темы лягут под него идеально: флеш-сообщения красивее всего живут рядом с отрисовкой страниц, а рендер по сессии — классика серверных шаблонов: страница приветствует по имени из sess.Get("user"), и всё, что мы положили в хранилище, наконец станет видно в HTML.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Кука — заголовок Set-Cookie в ответе и Cookie в запросе; Fiber прячет это за структурой fiber.Cookie (одиннадцать полей) и тремя методами: c.Cookie — запись, c.Cookies — чтение, c.ClearCookie — похоронка. В v3.5.0 метод записи живёт в res.go, а не в ctx.go.
- Нормализация записи: пустой Path становится "/", SessionOnly обнуляет MaxAge и Expires, SameSite=None или Partitioned сами добавляют Secure, незаданный SameSite — это Lax; невалидная кука молча игнорируется (invalid cookies are ignored, same approach as net/http).
- Атрибуты безопасности: HTTPOnly прячет куку от JS, но не от отправки; Secure — только HTTPS; SameSite=Lax — дефолт v3, None требует Secure. CORS и SameSite закрывают разные плечи одной атаки; в v3 есть middleware/csrf (умеет Session *session.Store) и middleware/encryptcookie (AES-GCM).
- Сессия: в куке только id session_id, данные — gob-карта в fiber.Storage. Два пути: session.New() + FromContext (автосейв после хендлера, Save no-op) или NewWithStore + store.Get (Save и Release обязательны; повторный Get — ErrSessionAlreadyLoadedByMiddleware).
- session.Config v3.5.0: Storage (default memory.New()), Extractor (extractors.FromCookie("session_id")), IdleTimeout (30 * time.Minute), AbsoluteTimeout (0 = нет, меньше IdleTimeout — паника), CookieSameSite "Lax", CookieSecure/CookieHTTPOnly/CookieSessionOnly false. CookieName и Expiration из v2 не существуют.
- Жизненный цикл: Save — gob-кодирование, Storage.SetWithContext, обновление куки; Destroy — DeleteWithContext плюс кука с MaxAge=-1; Regenerate — новый id и удаление старого — защита от фиксации при входе; Release возвращает сессию в пул.
- Хранилище fiber.Storage: Get возвращает nil, nil при отсутствии ключа; Set с exp 0 — без срока. Встроенная память — internal/storage/memory (из своего кода не импортируется); адаптеры Redis, Memcache, SQLite3 и другие — в модуле github.com/gofiber/storage; флеш-сообщения — паттерн Set перед redirect, Get+Delete после, отдельного middleware в v3.5.0 нет.
Домашнее задание
Дособерите remember-me честно: замените хранение имени в куке remember на случайный токен, кладите его в map[string]string рядом с задачами и проверяйте в третьей ветке стража; токен, которого нет в карте, должен давать 401. Добавьте AbsoluteTimeout: 12 * time.Hour в конфиг сессий и убедитесь, что приложение стартует (12 часов больше IdleTimeout — иначе паника из таблицы ловушек). Реализуйте счётчик посещений: sess.Set("visits", n+1) на /api/auth/me и сверьте, что два запроса подряд дают 1 и 2 — автосейв middleware работает, predict урока про забытый Save это уже объяснял. Напишите ветку выхода, которая гасит и сессию, и обе куки (Destroy плюс ClearCookie), и проверьте curl -b jar.txt -v, что в ответе Set-Cookie с Max-Age=-1. Наконец, соберите форму входа на html-странице пятого урока с чекбоксом remember и убедитесь, что браузер отправляет ровно то, что шлёт ваш curl.
Личные заметкиПусто
Вопросы и ответы по уроку
Почему у меня не работает c.Cookie("theme") для чтения — компилятор ругается?
В v3 запись и чтение разведены: c.Cookie(&fiber.Cookie{Name: "theme", ...}) — это запись структуры в Set-Cookie, а чтение — c.Cookies("theme") или с дефолтом c.Cookies("theme", "light"). В v2 c.Cookie(name) читала строку — чужие сниппеты с таким вызовом не компилируются в v3, и это самый частый источник путаницы. Проверяйте версию примера по сигнатуре: структура в аргументе — v3, строка — v2.
session.New() или NewWithStore(): что выбрать и почему падает store.Get?
Выбор определяется тем, нужен ли store вне HTTP-хендлеров. Если нет — session.New(): middleware сам загружает и сам сохраняет сессию, в хендлере только session.FromContext(c). Если нужен (GetByID из фоновых задач, админ-скрипты) — NewWithStore(config), который вернёт пару (fiber.Handler, *Store). Ошибка ErrSessionAlreadyLoadedByMiddleware возникает, когда middleware уже стоит в цепочке, а вы всё равно зовёте store.Get(c): сессия на запрос создаётся один раз, второй путь выдачи запрещён — берите FromContext.
Куда класть remember-токен: в куку или в сессию? И почему после входа нужен Regenerate?
Сессия с IdleTimeout 30 минут забудет пользователя за обедом, поэтому для «запомнить меня» ставят отдельную долгую куку с токеном — но значение куки всегда слово клиента, сервер обязан сверить токен с базой перед тем, как поверить. Regenerate при входе — правило из session.go: Regenerate generates a new session id and deletes the old one from storage. Если злоумышленник подсунул жертве известный ему id (фиксация сессии), после входа он умрёт вместе со старой личностью: новый id рождается KeyGenerator-ом и жертве не принадлежит.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Gin: самый популярный веб-фреймворк Go
Курс Gin для Go: маршрутизация, binding, валидация, middleware, JWT, WebSocket, тесты и деплой. 25 уроков с кодом и итоговым REST API.
- Уверенно владеть gin.Engine: маршруты, группы, версии
- Привязывать запросы к структурам (ShouldBindJSON и др.)
- Писать middleware и использовать встроенные
- Строить аутентификацию на JWT