---
title: "Cookie и сессии"
description: "Урок fiber cookie: структура fiber.Cookie, c.Cookies и ClearCookie, session middleware и хранилища, HTTPOnly, Secure, SameSite, flash и remember-me."
course: "Fiber: экспресс-подобный веб-фреймворк на Fasthttp"
url: "https://golangmaster.ru/courses/fiber/lessons/12-cookie-sessii/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 20
---

# Cookie и сессии

*Курс «Fiber: экспресс-подобный веб-фреймворк на Fasthttp», урок 12 из 25. ~20 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/fiber/lessons/12-cookie-sessii/*

Тема «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.

### 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?

- ✅ t=1; SameSite=None; Secure — метод увидит None и добавит Secure сам
- t=1; SameSite=None — Secure добавлять никто не станет, вы его не просили
- Кука не уйдёт вовсе: проверка hc.Valid() отклонит набор без Secure
- t=1; SameSite=Lax — None без Secure откатится к дефолтному Lax

В switch внутри метода Cookie ветка None выглядит так: sameSite = http.SameSiteNoneMode, а следом безусловное c.Secure = true — комментарий в исходнике поясняет: SameSite=None requires Secure=true per RFC and browser requirements. То же правило продублировано и в session middleware. Проверка Valid() тут ни при чём: с добавленным Secure кука валидна и уйдёт; ветка default по-прежнему отдаёт Lax — но None вы явно попросили, и валидатор не спорит. Практика: NEVER ставьте None на внутренние куки — это единственный режим, при котором кука гуляет в cross-site запросы, а значит, и главный кандидат на перехват; для всего остального в v3 стоит Lax.

### Чтение и похоронка: 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. Что он получит?

- ✅ Ничего о session_id: HTTPOnly прячет куку от JS, но она по-прежнему уходит с каждым запросом
- Все куки целиком: HTTPOnly защищает только от перехвата по проводу
- Ничего вообще: HTTPOnly запрещает браузеру отправлять куку на сервер
- Куку session_id с пустым значением: HTTPOnly обнуляет Value для JS

HTTPOnly работает в одну сторону: скрипт не читает куку через document.cookie, но браузер продолжает прикладывать её к каждому запросу — сервер её видит, сессия живёт. Именно поэтому связка HTTPOnly + id в куке + данные на сервере и есть учебник безопасности: украденный скриптом DOM бесполезен без самой куки, а куку из-за флага XSS не вытащит. Secure тут про другое плечо — канал: без него кука уехала бы по открытому HTTP в перехват. Ошибочный вариант «HTTPOnly запрещает отправку» описывает не куку, а совсем другие механизмы; браузер всегда отправляет HTTPOnly-куки. И помните из прошлого квиза: даже если XSS добрался до document.cookie, куки с Secure он прочитает только с той же страницы HTTPS — сам флаг канал не закрывает, он про то, куда кука уезжает.

### 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. Что окажется в хранилище после ответа?

- ✅ Сессия сохранится: middleware сам зовёт saveSession после хендлера — Save при middleware no-op
- Ничего: Save обязателен в любом раскладе
- Кука session_id уйдёт, но данные в Storage не попадут
- Хендлер вернёт 500: вызов Save пропущен нельзя

Док-комментарий Save договаривает контракт целиком: Save saves the session data and updates the cookie, а следом Note: If the session is being used in the handler, calling Save will have no effect and the session will automatically be saved when the handler returns. В middleware.go после c.Next() действительно стоит m.saveSession() — данные кодируются gob-ом, пишутся в Storage, кука обновляется. Исключение одно: session помечена Destroy — isDestroyed проверяется до сохранения, погашенная сессия записи не достойна. Оборотная сторона: тот же автосейв запишет и то, что вы клали «на подумать» и не собирались сохранять — забирать изменения нужно явно, Delete или перезаписью.

### Жизненный цикл: 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 не поедет на /static.** Атрибут Path решает, на какие пути браузер вообще отправляет куку. Кука remember с Path=/api уезжает только на маршруты, начинающиеся с /api, — статика из пятого урока её никогда не увидит. Это и защита (кука авторизации не мелькает в запросах к публичным файлам), и ловушка: страж на /api не увидит куку, поставленную с Path=/, — нет, увидит, наоборот — а вот кука с Path=/api/auth не дотянет до /api/tasks. Правило: сужайте Path до префикса, где кука реально нужна, и держите в уме, что Path=/api покрывает и /api/auth, и /api/tasks сразу.

### Забытый Save: что увидит второй запрос?

```go
// Ручной путь: 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
```

**Что выведет программа?**

- ✅ Оба раза 1: без Save нет ни Set-Cookie, ни записи в Storage — каждый запрос рождает новую сессию
- 1 и 2: Set сам попадает в память store при вызове
- 1 и 2: счётчик жил в куке items и вернулся со вторым запросом
- Ошибка на втором запросе: сессия без Save считается битой

При ручном пути без Save не происходит ничего из жизненно важного: saveSessionWithContext не вызывался, значит, Storage.SetWithContext не писался и setSession не ставил куку session_id. Второй curl приходит без id — extractor FromCookie ничего не находит, KeyGenerator рождает новый id, Fresh() равен true, Get("items") возвращает nil, приведение к int даёт 0, и ответ снова 1. Никаких ошибок, никаких логов — тихая потеря состояния, худший сорт бага. Контракт запомнить легко: middleware в цепочке — Save не нужен и no-op; store.Get руками — Save обязателен, а Release возвращает сессию в пул.

### Хранилища: 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 — для разработки идеально, для продакшена означает потерю всех сессий на рестарте: переживают перезапуск только внешние адаптеры.

### Кука без срока: сколько проживёт?

```go
// SessionOnly не тронут, MaxAge не задан, Expires нулевой
c.Cookie(&fiber.Cookie{
        Name:  "intro",
        Value: "shown",
})
```

**Что выведет программа?**

- ✅ Сессионная кука без Expires и Max-Age: живёт, пока открыт браузер
- Кука на год: Fiber подставит дефолтный срок из конфига
- Кука не уйдёт: hc.Valid() отклонит набор без срока
- Кука с Expires = сейчас: браузер удалит её немедленно

SessionOnly false — обнуления не будет; MaxAge 0 и нулевой Expires уходят в fasthttp как SetMaxAge(0) и SetExpire(zero) — атрибуты просто не попадут в заголовок. Дальше работает правило, которое session.go цитирует прямо из MDN: Cookies are also session cookies if they do not specify the Expires or Max-Age attribute. Получается сессионная кука — ровно то, что явно даёт SessionOnly: true, только без флага. Разница тонкая, но реальная: у SessionOnly семантика «я сознательно хочу куку-до-закрытия», а пустой срок — это часто забытый MaxAge, и читатель кода не отличит умысел от опечатки. Совет практический: срок указывайте всегда, SessionOnly — только когда он и есть умысел, а проверять результат удобно curl -v: строка Set-Cookie без Max-Age и Expires — значит, кука умрёт с браузером.

### 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.

```go
// Каркас: 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)
        })
}
```

Подсказка: Пара из NewWithStore — handler, store; в хендлере сессию отдаёт session.FromContext(c); гасит сессию sess.Destroy(); читает куку c.Cookies("remember"), ставит структуру c.Cookie(&fiber.Cookie{...}).

Правильные ответы в пропусках: `store`, `NewWithStore`, `FromContext`, `Destroy`, `c.Cookies`, `c.Cookie`

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

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

Правильный порядок строк:

1. `curl -c jar.txt -d "username=anna&password=s3cret&remember=on" localhost:8080/api/auth/login`
2. `helmet ставит защитные заголовки и пропускает запрос дальше`
3. `cors добавляет Access-Control-Allow-Origin и тоже пропускает`
4. `logger пишет строку запроса в консоль`
5. `session middleware ищет куку session_id: её нет — KeyGenerator создаёт свежий id`
6. `хендлер разбирает форму Bind().Form и зовёт Regenerate: id меняется ещё раз`
7. `sess.Set кладёт user в gob-кодируемую карту данных сессии`
8. `c.Cookie ставит куку remember на 30 дней: HTTPOnly, Secure, SameSite=Lax`
9. `после выхода хендлера middleware сам сохраняет сессию и ставит Set-Cookie: session_id=...`

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

| Событие | Что увидите | Лечение |
| --- | --- | --- |
| 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 |

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

- **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).
- **c.Cookies / c.ClearCookie** — c.Cookies(key, default...) — чтение из req.go: нет куки — пустая строка или default. c.ClearCookie(key...) — похоронка из res.go: DelClientCookie с погасшим сроком; без аргументов гасит все куки запроса. Сервер куки не стирает — он просит браузер считать их истёкшими.
- **HTTPOnly / Secure / SameSite** — HTTPOnly прячет куку от document.cookie, но не от отправки. Secure — только HTTPS; SameSite=Lax (дефолт v3) режет cross-site POST, Strict — и переходы по ссылкам, None — едет всегда и требует Secure (fiber добавит сам). CORS из восьмого урока про чтение ответа, SameSite — про отправку куки.
- **session.New / NewWithStore / FromContext** — session.New() — только хендлер (store discard-ится); NewWithStore() — пара (Handler, *Store). В хендлере session.FromContext(c) даёт *Middleware с Set/Get/Delete/Destroy/Regenerate. Повторная загрузка через store.Get вернёт ErrSessionAlreadyLoadedByMiddleware. Глобальная session.Store из v2 в v3 заменена инстансами NewStore/NewWithStore.
- **session.Config v3.5.0** — Storage (default memory.New()), KeyGenerator (utils.SecureToken), Extractor (extractors.FromCookie("session_id")), IdleTimeout (30 * time.Minute, обновляется на Save), AbsoluteTimeout (0 = нет; меньше IdleTimeout — паника), CookieSameSite ("Lax"), CookieSecure/CookieHTTPOnly/CookieSessionOnly (false). CookieName и Expiration из v2 в конфиге v3 не существуют.
- **Save / Destroy / Regenerate** — Save — данные в Storage gob-ом плюс обновление куки; при middleware no-op — автосейв после хендлера (кроме Destroy-нутых). Destroy — DeleteWithContext из Storage плюс кука с MaxAge=-1. Regenerate — новый id, старый удалён: правило фиксации при входе. Ручной путь: Save обязателен, Release возвращает сессию в пул.

### Запуск, шесть сценариев 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 — из [седьмого урока](/courses/fiber/lessons/07-svoi-middleware/), разграничение CORS и SameSite — из [восьмого](/courses/fiber/lessons/08-cors/), форма входа и Bind().Form — из [одиннадцатого](/courses/fiber/lessons/11-formy-fajly/); полный план — на странице [курса Fiber](/courses/fiber/). Песочница сайта сетевые сценарии не запускает — runner-ов в уроках Fiber нет, интерактивы здесь quiz, predict, fill, order и флешкарты; каркас собирайте локально в терминале.

Дальше — тринадцатый урок «HTML-шаблоны»: шаблонизаторы, html/template и рендер страниц. Сегодняшние темы лягут под него идеально: флеш-сообщения красивее всего живут рядом с отрисовкой страниц, а рендер по сессии — классика серверных шаблонов: страница приветствует по имени из sess.Get("user"), и всё, что мы положили в хранилище, наконец станет видно в HTML.

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

- Кука — заголовок 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-ом и жертве не принадлежит.
