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

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

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

Урок 12 из 25 ~20 мин +50 XP

Cookie и сессии

Тема «fiber cookie» закрывает главный пробел первых одиннадцати уроков: каждый запрос приходил на сервер человеком без прошлого. HTTP между запросами не помнит ничего, а браузер умеет — если сервер вручил ему куку. Сегодня разбираем оба слоя памяти. Первый — обычные куки: fiber.Cookie — структура для записи, c.Cookies — чтение, ClearCookie — похоронка. Второй — полноценные fiber сессии через middleware/session: id в куке session_id, данные в gob-кодированном хранилище, методы Set, Save, Destroy и Regenerate. Всё сверено с исходниками тега v3.5.0: res.go, req.go, middleware/session/session.go, config.go и store.go. Канон курса продолжается: API на :8080, группа /api со стражем requireAPIKey("s3cret") из седьмого урока, слои helmet, cors и logger, форма входа через Bind().Form из одиннадцатого урока. Цель урока — remember-me: инстанс из седьмого урока научится узнавать браузер и без заголовка X-API-Key.

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

HTTP — протокол без состояния: каждый запрос для сервера рождён заново, никто не помнит, что пять минут назад этот же браузер логинился, складывал товары в корзину или выбрал тёмную тему. Механизм чинит это на уровне заголовков. Сервер в ответе отдаёт строку Set-Cookie: имя=значение плюс атрибуты срока и безопасности; браузер сохраняет куку и с каждым следующим запросом к этому домену возвращает её в заголовке Cookie. Память живёт не на сервере и не в протоколе, а у клиента — и это определяет всё: и возможности, и ловушки сегодняшней темы «fiber cookie».

Fiber здесь работает вплотную к fasthttp: чтение кук — это разбор заголовка Cookie запроса, запись — добавление Set-Cookie в ответ. Но над сырыми заголовками поднят типизированный слой: структура fiber.Cookie с одиннадцатью полями и три метода на контексте — запись, чтение и удаление. Мы пройдём по исходникам тега v3.5.0 в порядке жизни запроса: сначала поставим куку, потом прочитаем, потом погасим — и только затем поднимемся на уровень выше, к сессиям, где в куке лежит лишь идентификатор, а настоящие данные — в серверном хранилище.

Сверимся с первоисточником. В корневом 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. Что он получит?

Теперь второй слой — fiber сессии. Идея проста: кука плоха как хранилище данных. Она строковая, у неё жёсткий размер (браузеры держат порядка четырёх килобайт на куку), она уезжает на клиент и там читается и правится руками — в devtools любую куку видно и можно подделать, а HTTPS и флаги защищают канал, но не сам файл кук на чужой машине. Сессия переворачивает схему: в куке живёт только случайный идентификатор session_id, а данные — gob-кодированная карта ключ-значение — лежит в Storage на сервере. Клиент может подделать id? Может попробовать, но валидный id — 256-битный токен из KeyGenerator, угадать его нельзя, а чужой id либо не найдётся в хранилище, либо будет разобран как чужая сессия. Клиент может прочитать id? Да, но id — не секрет, а адрес: полезность имеет лишь в паре с хранилищем.

Отсюда правило распределения: «кто я» и всё, что даёт права, — в сессию; безобидная косметика вроде свёрнутой панели — можно и в куку. Любое значение куки — это слово клиента, и строить на нём авторизацию нельзя; любое значение сессии — слово сервера самому себе. Таблица ниже фиксирует разницу, а дальше идём в исходники middleware/session — там эта схема реализована буквально.

Свойство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: тот самый факт из середины урока, что сервер куки не стирает, а просит браузер считать истёкшей.

Забытый 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.

// Каркас: middleware сессий и маршрут выхода func setup(app *fiber.App) { // NewWithStore вернёт и хендлер, и store handler, := session.(session.Config{ CookieHTTPOnly: true, }) app.Use(handler) app.Post("/api/auth/logout", func(c fiber.Ctx) error { // сессию загрузил middleware — достаём из контекста sess := session.(c) // данные из Storage, кука session_id — в прошлое if err := sess.(); err != nil { return err } // долгая кука remember гасится отдельно if token := ("remember"); token != "" { // похоронка: кука с истёкшим сроком (&fiber.Cookie{ Name: "remember", Value: "", Path: "/api", MaxAge: -1, HTTPOnly: true, }) } return c.SendStatus(fiber.StatusNoContent) }) }

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

Расставьте шаги запроса на вход от команды в терминале до Set-Cookie в ответе.

Строки

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

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

Ловушки кук и сессий: событие, симптом, лечение#

СобытиеЧто увидитеЛечение
SameSite: "None" без SecureFiber добавит 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 меньше IdleTimeoutpanic при старте: [session] AbsoluteTimeout must be greater than or equal to IdleTimeoutзадавать AbsoluteTimeout >= IdleTimeout или оставить 0
кука без MaxAge и Expiresсессионная кука: умерла с закрытием браузерауказывать срок явно; SessionOnly — только как осознанный выбор
IdleTimeout в CookieSessionOnly-кукеданные в store живут idleTimeout, кука умерла с браузеромпомнить комментарий конфига: CookieSessionOnly не удаляет данные из store

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

Термин

fiber.Cookie

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

Структура с одиннадцатью полями: Expires, Name, Value, Path, Domain, SameSite, MaxAge, Secure, HTTPOnly, Partitioned, SessionOnly. Метод c.Cookie(*fiber.Cookie) из res.go ставит Set-Cookie; аргумент read-only, нормализация — на копии; невалидная кука молча игнорируется (invalid cookies are ignored, same approach as net/http).

1 / 6

Запуск, шесть сценариев 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.

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

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

Почему у меня не работает 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-ом и жертве не принадлежит.

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

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

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

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

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

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

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

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

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