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

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

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

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

Cookie и сессии

Наш трекер до сих пор считал каждый запрос первым: JSON-конверты из [урока про ответы](/courses/gin/lessons/05-json-otvety/) честно отвечали, но кто спрашивает — сервер не знал и не помнил. HTTP stateless: между запросами сервер ничего не хранит, и разрыв закрывает cookie — строка, которую сервер отправил в Set-Cookie, а браузер возвращает в каждом следующем запросе. На cookie строятся два слоя: сами значения (предпочтения, флаги) и сессии — словарь на сервере, до которого из браузера добираются через gin-contrib/sessions. Разберём семь аргументов c.SetCookie и флаги HttpOnly, Secure, SameSite, соберём логин на сессиях, заглянем внутрь cookie-хранилища и выясним, почему подпись не шифрование, куда ставить redis, как работают флеш-сообщения и почему забытый session.Save() тихо ломает логин. Звенья повторяем из [урока про кастомные middleware](/courses/gin/lessons/07-kastomnye-middleware/), группы — из [урока про группы маршрутов](/courses/gin/lessons/10-gruppy/), а экран от XSS-кражи cookie уже экранировал [урок про шаблоны](/courses/gin/lessons/12-shablony/).

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

HTTP по спецификации не помнит ничего: каждый запрос живёт сам по себе, и сервер в принципе не обязан связывать два запроса от одного браузера. Для статических страниц это нормально, но трекер, который после логина снова считает вас гостем, никому не нужен. gin cookie — это ровно две обёртки над стандартной библиотекой: c.SetCookie ставит заголовок Set-Cookie в ответе, c.Cookie читает заголовок Cookie из запроса. Механика RFC 6265 простая: сервер в ответе говорит браузеру «запомни пару goma=1 и возвращай мне её»; браузер сохраняет и с каждым запросом к этому хосту отправляет её обратно в заголовке Cookie — сервер снова «узнаёт» клиента. Всё состояние при этом лежит у клиента: cookie — не память сервера, а памятка, которую он выдал гостю. Отсюда два следствия, которые определяют весь урок. Первое: cookie можно читать и подделывать — пользователь открывает DevTools и видит значения целиком, поэтому в чистой cookie не хранят ничего секретного. Второе: cookie ограничена — примерно 4 КиБ на пару и лимиты на домен, куда словарь задач не упакуешь. Для простых вещей — «пользователь закрыл онбординг», «выбрал тёмную тему», «последний раз был вот тут» — чистой cookie хватает. Для полноценной аутентификации поверх cookie строят сессии: словарь живёт на сервере, а в cookie едут только ссылка на него — идентификатор или подписанный пакет. Связку «простая cookie — c.SetCookie», «сессия — middleware из gin-contrib» и разберём в этом порядке: сначала механика значения, потом слой над ней. Хендлер возьмём из арсенала трекера: счётчик визитов — минимальный пример «узнавания» клиента без единой строчки серверного состояния.

Первый cookie: счётчик визитов

GO
package main

import (
        "net/http"
        "strconv"

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

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

        r.GET("/counter", func(c *gin.Context) {
                n := 1
                if v, err := c.Cookie("visits"); err == nil {
                        if parsed, perr := strconv.Atoi(v); perr == nil {
                                n = parsed + 1
                        }
                }
                c.SetCookie("visits", strconv.Itoa(n), 3600, "/", "localhost", false, true)
                c.JSON(http.StatusOK, gin.H{"visits": n})
        })

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

c.Cookie("visits") возвращает пару (value, error): у первого визита cookie нет, и error — это http.ErrNoCookie, штатный исход, а не сбой. c.SetCookie ставит Set-Cookie в ответ; откройте страницу дважды — счётчик растёт. Проверка curl-ом: curl -c jar.txt localhost:8080/counter пишет cookie в файл, curl -b jar.txt localhost:8080/counter отправляет её обратно.

c.SetCookie(name, value, maxAge, path, domain, secure, httpOnly) — обёртка над http.SetCookie, и все семь аргументов превращаются в поля структуры http.Cookie. Имя и значение — ASCII-строки; значение — любое слово клиента, так что сериализуйте и проверяйте его сами, а не верьте, что там окажется то, что вы положили. maxAge — секунды жизни, и здесь три режима: положительное число — «живи столько-то секунд» (Max-Age=3600 — час); ноль — заголовок Max-Age вообще не ставится, cookie становится сессионной и живёт до закрытия браузера; отрицательное число — Max-Age=0, cookie приказывает браузеру немедленно удалить старое значение. Это самый недооценённый аргумент: logout, который хочет «стереть» cookie, обязан повторить её Set-Cookie с тем же именем, path и domain и отрицательным maxAge — иначе пара продолжит ездить. path — префикс URL, на который браузер согласен отправлять cookie: «/» едут на все маршруты, «/api» — только на маршруты, начинающиеся с этого префикса; более узкий path — более узкий размах, но помните, что несовпадающий path не удаляет cookie, а создаёт вторую. domain — по умолчанию cookie привязана к хосту, который её выдал, и поддомены её не видят; указание domain=.example.com разделяет её на все поддомены — не включайте без причины. secure — отправлять только по HTTPS; httpOnly — запретить JavaScript доступ через document.cookie. Важная дыра gin-обёртки: у c.SetCookie нет аргументов для SameSite и Expires — их ставит только полная форма: http.SetCookie(c.Writer, &http.Cookie{Name: "goma", Value: "1", MaxAge: 3600, Path: "/", HttpOnly: true, Secure: true, SameSite: http.SameSiteLaxMode}). c.Writer — это http.ResponseWriter из урока про middleware, и http.SetCookie просто дописывает заголовок в его карту. Пользуйтесь короткой формой, пока хватает её семи аргументов, и полной — когда дошли до SameSite.

Проверка: три возраста maxAge

Хендлер вызывает c.SetCookie("goma", "1", 0, "/", "localhost", false, true). Какую cookie получит браузер?

Три флага — три линии обороны, и включать их нужно осознанно, а не «когда-нибудь в проде». HttpOnly запрещает JavaScript читать cookie через document.cookie. Зачем: если XSS просочился сквозь экранирование из урока про шаблоны — одна забытая {{ .Name | safeHTML }} — скрипт атакующего выполняется на вашей странице и первым делом тянет document.cookie наружу. HttpOnly не чинит XSS, но выжигает самую жирную награду: украденную сессию нельзя переиспользовать с чужой машины. secure=true — браузер не отправит cookie по незащищённому http: без него первый же открытый Wi-Fi превратит cookie в подарок попутчику, слушающему трафик. В разработке на http://localhost флаг придётся временно ставить false — или поднимать локальный TLS. SameSite — защита от того, чтобы браузер приложил вашу cookie к запросам с чужих сайтов; деталей про CSRF-сценарий — в уроке про CORS и безопасность, здесь фиксируем режимы: http.SameSiteLaxMode — cookie едет только в переходах верхнего уровня по GET, то есть по обычной ссылке с чужого сайта cookie приложится, а POST-формой злоумышленника — нет; Strict — вообще не едет с чужих сайтов, вплоть до того, что переход по ссылке из мессенджера придёт на сайт «незалогиненным»; None — едет всегда, но требует secure=true и нужен только для честного кросс-сайтового встраивания. Современные браузеры по умолчанию ведут себя как Lax. Отдельное правило гигиены: Path и Domain — про минимальный размах. Cookie для админки не обязана летать на /api, а для статического CDN — вообще не нужна: чем уже область видимости, тем меньше поверхностей. И последнее: в cookie не кладут пароли, токены карт и персональные данные — слово клиента читается как открытая книга, вплоть до того, что пользователь вправе его отредактировать.

Чистая cookie хорошо возит флаги и плохо — аутентификацию: значение читается, редактируется и ограничено 4 КиБ. сессии gin решают это надстройкой: словарь «ключ-значение» живёт на сервере (или в подписанной cookie — о хранилищах ниже), а браузеру достаётся либо непрозрачный идентификатор, либо упакованный пакет. Механика слоя — пакет github.com/gin-contrib/sessions: go get github.com/gin-contrib/sessions — и два шага. Первый: при старте создайте хранилище и повесьте middleware: store := cookie.NewStore([]byte(os.Getenv("SESSION_SECRET"))); r.Use(sessions.Sessions("goma", store)) — «goma» здесь имя cookie, в которой сессия будет жить, одно звено на всё приложение, ставится до маршрутов, как и все звенья из урока про кастомные middleware. Второй: в хендлере session := sessions.Default(c) — интерфейс с четырьмя методами словаря: session.Get("user") возвращает any и nil, если ключа нет; session.Set("user", username) кладёт значение; session.Delete и session.Clear вычищают ключ или всю сессию. Критичное, что забывают все: изменения копятся в сессии в памяти запроса и НЕ попадают в ответ автоматически — их публикует session.Save(). Забыли Save — cookie уйдёт без изменений, пользователь вроде «залогинился», но при следующем запросе снова гость; ошибки при этом нет, баг тихий, и живёт он до первого прогона руками. Хранилище настраивается флагами: store.Options(sessions.Options{MaxAge: 3600, Path: "/", HttpOnly: true, Secure: false, SameSite: http.SameSiteLaxMode}) — те же поля http.Cookie, что мы разбирали выше, только применяются ко всем сессиям этого store. Обратите внимание: имя cookie «goma» и секрет SESSION_SECRET — это всё, что злоумышленник узнаёт о вашей сессии; словарь остаётся на сервере, и в этом весь смысл слоя.

Логин на сессиях: /login, /profile, /logout

GO
store := cookie.NewStore([]byte(os.Getenv("SESSION_SECRET")))
store.Options(sessions.Options{
        MaxAge:   3600,
        Path:     "/",
        HttpOnly: true,
        Secure:   false, // в проде за HTTPS — true
})
r.Use(sessions.Sessions("goma", store))

r.POST("/login", func(c *gin.Context) {
        session := sessions.Default(c)
        username := c.PostForm("username")
        if username == "" {
                c.JSON(http.StatusBadRequest, gin.H{
                        "error":   "BAD_REQUEST",
                        "message": "укажите username",
                })
                return
        }
        session.Set("user", username)
        if err := session.Save(); err != nil {
                c.JSON(http.StatusInternalServerError, gin.H{"error": "INTERNAL"})
                return
        }
        c.JSON(http.StatusOK, gin.H{"user": username})
})

r.GET("/profile", func(c *gin.Context) {
        session := sessions.Default(c)
        user := session.Get("user")
        if user == nil {
                c.JSON(http.StatusUnauthorized, gin.H{
                        "error":   "UNAUTHORIZED",
                        "message": "войдите через POST /login",
                })
                return
        }
        c.JSON(http.StatusOK, gin.H{"user": user})
})

r.POST("/logout", func(c *gin.Context) {
        session := sessions.Default(c)
        session.Clear()
        _ = session.Save()
        c.JSON(http.StatusOK, gin.H{"message": "вы вышли"})
})

sessions.Default(c) работает только после звена sessions.Sessions — иначе middleware не положит сессию в контекст. session.Get возвращает any: nil значит «не залогинен», и это проверка на 401. В logout Clear плюс Save: без Save очистка не доедет до cookie. c.PostForm разбирает form-encoded тело, как в уроке про параметры.

cookie.NewStore([]byte(secret)) — самое неожиданное хранилище сессий: словарь не живёт на сервере вовсе, а сериализуется, кодируется base64 и кладётся прямо в cookie у клиента. Отсюда парадоксальное свойство: сервер stateless — рестарт процесса не стирает ни одной сессии, кластер из десяти узлов не нуждается в общем redis, а нагрузка на память нулевая. Но клиент держит сессию в руках — что мешает ему дописать себе role: "admin"? Мешает подпись. cookie.NewStore принимает до двух ключей: hashKey и blockKey. hashKey обязателен и должен быть 32 или 64 байта — им считается HMAC-SHA256 (или SHA-512) над содержимым; в cookie попадает значение и подпись через вертикальную черту, а на каждом запросе middleware пересчитывает HMAC и сравнивает. Байт поменян — подпись не сходится — cookie отбрасывается, сессия становится пустой: подделка отсечена. Именно поэтому ключ из os.Getenv, а не литерал в коде: секрет в git — это подписанные сессии любого, кто скачал репозиторий. blockKey — необязательный второй ключ длиной 16, 24 или 32 байта: включает AES-шифрование содержимого. Без него база64-строку видно целиком: скормите её любому декодеру — получите JSON сессии с username и прочим. Подпись гарантирует целостность, но не конфиденциальность, и это главный вывод про cookie-хранилище: не кладите в сессию ничего, что не готовы показать пользователю, — идентификатор пользователя, имя, язык, тему — можно; роль, секреты, номер карты — нельзя, роли и секреты проверяйте на сервере по идентификатору. Отдельная гигиена — ротация: смена hashKey делает все существующие сессии невалидными, и все пользователи разом разлогинятся; если ключ заподозрили в утечке, это фича, а не баг, но плановую ротацию лучше не показывать продам.

Забытый Save(): куда уехал логин

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

r.POST("/login", func(c *gin.Context) {
        session := sessions.Default(c)
        username := c.PostForm("username")
        if username == "" {
                c.JSON(http.StatusBadRequest, gin.H{"error": "BAD_REQUEST"})
                return
        }
        session.Set("user", username)
        // session.Save() забыли
        c.JSON(http.StatusOK, gin.H{"user": username})
})

r.GET("/profile", func(c *gin.Context) {
        session := sessions.Default(c)
        if user := session.Get("user"); user != nil {
                c.JSON(http.StatusOK, gin.H{"user": user})
                return
        }
        c.JSON(http.StatusUnauthorized, gin.H{"error": "UNAUTHORIZED"})
})

sessions — интерфейс над хранилищами, и от выбора хранилища зависит, где живут данные и что переживает аварии. cookie.NewStore — данные у клиента, подписанные HMAC; сервер ни о чём не помнит, рестарт и горизонтальное масштабирование бесплатны, лимит — те самые 4 КиБ и правило «ничего секретного». redis.NewStore(10, "tcp", "localhost:6379", "", "") — сессии живут в redis, а в cookie у клиента только непрозрачный идентификатор; размер словаря перестаёт волновать, секретные поля — тоже, и появляется возможность выкинуть сессию принудительно: удалили ключ в redis — пользователь разлогинен, даже если cookie жива. Цена — зависимость от redis и общий узел для всех реплик: классическая замена stateless на stateful. memstore из того же пакета хранит сессии в памяти процесса — идеально для тестов и мгновенно умирает при рестарте, в прод не тащить. Выбор сводится к трём вопросам. Сколько данных? Пара строк — cookie-хранилище, сотни килобайт или корзина покупок — redis. Сколько узлов? Один — cookie-хранилище всё ещё ок, кластер — cookie-хранилище тоже ок, но только пока данные безобидные; redis — когда нужен единый источник правды. Нужен ли мгновенный logout и бан? С cookie-хранилищем «отозвать сессию» невозможно — она у клиента; с redis — один DEL. Для нашего трекера cookie-хранилище честно тянет первые месяцы, а дальше, когда появятся корзины, права и «выйти на всех устройствах», переезд на redis — замена одной строки создания store, весь код хендлеров не меняется: interface Store у пакетов один и тот же.

ИнструментЧто делаетКогда брать
c.SetCookie(name, value, maxAge, path, domain, secure, httpOnly)ставит Set-Cookie в ответпростой флаг или предпочтение
c.Cookie(name)читает Cookie из запроса, http.ErrNoCookie если нетодно значение без сессий
http.SetCookie(c.Writer, &http.Cookie{...})полный контроль: SameSite, Expiresкогда короткой формы мало
cookie.NewStore(hashKey, blockKey)cookie-хранилище: base64 плюс HMACstateless, немного нечувствительных данных
redis.NewStore(10, "tcp", "localhost:6379", "", "")сессии в redis, в cookie только idкластер, большие данные, отзыв сессии
memstore.NewStore([]byte(secret))сессии в памяти процессатесты; в прод не тащить
sessions.Sessions("goma", store)middleware сессийr.Use один раз до маршрутов
session := sessions.Default(c)доступ к сессии в хендлереGet/Set/Delete/Clear
session.Save()пишет изменения в ответпосле каждого Set/Delete/Clear
store.Options(sessions.Options{...})MaxAge, HttpOnly, Secure, SameSiteнастройка всех сессий store

Флеш-сообщения: AddFlash и сценарии после редиректа#

У сессий есть спецрежим для одноразовых значений — флеш-сообщения. session.AddFlash("Задача сохранена") кладёт значение, которое живёт ровно до первого чтения: session.Flashes() возвращает слайс всех накопленных флешей и очищает их, так что при следующем запросе список пуст. Классический сценарий — Post-Redirect-Get: пользователь отправляет форму POST-ом, и просто «ответить 200» значит обречь его на повторную отправку формы по F5; правильный флоу — обработать POST, положить в сессию «что получилось», ответить редиректом на страницу списка и уже там показать сообщение. В трекере это выглядит так: POST /todos валидирует TodoInput, при ошибке AddFlash("Проверьте поля") и redirect 303 на /, при успехе AddFlash("Задача добавлена") и тот же redirect; страница / читает Flashes и рисует сообщение над списком. Не забудьте Save и после чтения флешей — очистка тоже изменение сессии. Второй слой сценариев — прочие одноразовые флаги: «вы уже видели онбординг», «показать предупреждение о входе с нового устройства», закрытие диалогов. Третий слой — идентификация владельца задач: звено из урока про кастомные middleware достаёт session.Get("user"), кладёт в c.Set("user", ...), и все хендлеры ниже читают владельца из контекста, фильтруя задачи. Это ровно та архитектура, на которой в следующем уроке построят JWT-гвард — только источником идентичности станет подписанный токен, а не словарь на сервере. Заметьте прогресс: в четвёртом уроке трекер принимал задачи от анонимов, в этом — знает, кто их прислал, и не покажет чужой список.

Post-Redirect-Get с флешем

GO
r.POST("/todos", func(c *gin.Context) {
        session := sessions.Default(c)
        var input TodoInput
        if err := c.ShouldBind(&input); err != nil {
                session.AddFlash("Проверьте поля формы")
                _ = session.Save()
                c.Redirect(http.StatusSeeOther, "/")
                return
        }
        todos = append(todos, Todo{Title: input.Title, Done: false})
        session.AddFlash("Задача добавлена")
        _ = session.Save()
        c.Redirect(http.StatusSeeOther, "/")
})

r.GET("/", func(c *gin.Context) {
        session := sessions.Default(c)
        flashes := session.Flashes()
        _ = session.Save() // записать очистку флешей
        c.HTML(http.StatusOK, "index.html", gin.H{
                "tasks":  todos,
                "flash":  flashes,
        })
})

303 See Other — канонический статус редиректа после POST: браузер повторит GET без resend-предупреждения. TodoInput — структура с тегами binding из урока про привязку, c.HTML — шаблоны из урока про HTML. Flashes() читает и опустошает список одним вызовом; Save после чтения обязателен, иначе «пусто» не запишется и сообщение вернётся при следующем запросе.

Проверка: где живут данные cookie-хранилища

Приложение собрано на cookie.NewStore([]byte(secret)). Где физически лежит значение session.Get("user") между запросами?

Cookie отлаживается curl-ом лучше, чем любым фреймворком, — через cookie jar. curl -c jar.txt -X POST -d "username=om" localhost:8080/login — флаг -c сохраняет присланное Set-Cookie в файл; откройте jar.txt: там имя, значение, домен, path и срок. curl -b jar.txt localhost:8080/profile — флаг -b отправляет сохранённое обратно, и профиль отвечает вашим пользователем. Так прогоняется весь флоу: логин, профиль, logout, повторный профиль с 401 — без браузера и без фантазий о том, «что там сервер подумал». В браузере путь тот же: DevTools, вкладка Network — смотрите заголовки Set-Cookie ответа и Cookie запроса, вкладка Application — таблица всех cookies с флагами HttpOnly, Secure, SameSite, сроком и размером. Три частые грабли из практики. Первая: cookie не приходит на нужный маршрут — проверьте path: логин поставил Path=/login, а профиль ждёт на / — браузер в этот path не заходит; удаляйте cookie тем же path, каким ставили, иначе получите вторую с тем же именем. Вторая: значение рассыпалось на мусор — cookie любит запятые и пробелы: кодируйте значение (url-кодирование, base64), прежде чем класть. Третья: «работает в Postman, не работает в браузере» — почти всегда SameSite или Secure: Postman не браузер, он cookie прикладывает всегда, а браузер уважает флаги. И помните из урока про логирование: gin.Logger пишет только статус и время — cookie в логи не попадают, и слава богу: в сессии живёт аутентификация, а логи читает вся команда.

Проверка: cookie не доезжает

Логин ставил cookie через c.SetCookie("flag", "1", 3600, "/login", ...), но на GET /profile заголовка Cookie нет. Самая вероятная причина?

Соберите флоу Post-Redirect-Get

Допишите обработку формы: достать сессию, положить сообщение-флеш, опубликовать изменения и редиректнуть на список.

func createTodo(c *gin.Context) { session := .Default(c) var input TodoInput if err := c.ShouldBind(&input); err != nil { session.AddFlash("Проверьте поля формы") // 2. Изменения не едут в cookie без этого вызова. _ = session.() c.Redirect(http., "/") return } todos = append(todos, Todo{Title: input.Title, Done: false}) session.("Задача добавлена") _ = session.Save() c.Redirect(http.StatusSeeOther, "/") }

Соберите путь сессии от логина до профиля

Cookie-хранилище, POST /login с username=om, затем GET /profile: разложите события по порядку.

Строки

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

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

Карточки: cookie и сессии

Термин

c.SetCookie и семь аргументов

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

SetCookie(name, value, maxAge, path, domain, secure, httpOnly) — обёртка над http.SetCookie. maxAge: плюс — секунды жизни, ноль — сессионная cookie (без Max-Age, до закрытия браузера), минус — Max-Age=0 и удаление. SameSite и Expires доступны только через http.SetCookie(c.Writer, &http.Cookie{...}).

1 / 6
  • HTTP stateless: cookie — штатный способ носить состояние, сервер ставит Set-Cookie, браузер возвращает в Cookie; всё значение лежит у клиента и читается им целиком.
  • maxAge в c.SetCookie — секунды: ноль — сессионная cookie, минус — удаление, плюс — срок жизни; logout обязан повторить Set-Cookie с теми же name/path/domain и отрицательным maxAge.
  • HttpOnly и Secure — всегда, SameSite — через http.SetCookie: XSS не прочитает сессию, http-подслушиватель не получит её, чужой сайт не отнимет.
  • Cookie-хранилище подписывает HMAC, но не шифрует без blockKey: в сессии — id и предпочтения, роли и секреты проверяются на сервере.
  • session.Save() после каждого Set/Delete/Clear/Flashes: без него изменения исчезают молча — ошибок нет, флоу сломан.
  • Мало данных и один сервер — cookie.NewStore; большие данные, кластер или отзыв сессий — redis.NewStore; переезд — замена одной строки.
  • Отладка — curl с jar: -c сохраняет, -b отправляет; «не работает в браузере, но работает в Postman» — почти всегда SameSite, Secure или несовпавший Path.

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

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

Вывод

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

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

  • HTTP stateless; gin cookie — это c.SetCookie и c.Cookie над заголовками Set-Cookie/Cookie: значение живёт у клиента и читается им целиком.
  • maxAge — секунды: плюс — срок, ноль — сессионная cookie до закрытия браузера, минус — удаление; logout повторяет Set-Cookie с теми же name/path/domain и отрицательным maxAge.
  • HttpOnly и Secure — базовый минимум, SameSite (Lax по умолчанию в браузерах) ставится через http.SetCookie(c.Writer, &http.Cookie{...}) — у короткой формы gin его нет.
  • сессии gin — middleware sessions.Sessions("goma", store) и sessions.Default(c) с Get/Set/Delete/Clear; изменения публикует только session.Save() — забытый Save это тихий неработающий логин.
  • Cookie-хранилище подписывает данные HMAC (hashKey 32/64 байта), шифрует по желанию (blockKey 16/24/32): подпись не шифрование — секретных данных в сессии быть не должно; ключи из env, не из git.
  • redis.NewStore перекладывает сессии на сервер и разрешает отзыв через DEL; cookie-хранилище stateless и переживает рестарт; переезд — замена одной строки, хендлеры не меняются.
  • AddFlash и Flashes — одноразовые сообщения для Post-Redirect-Get с 303; это и заготовка идентификации пользователя, которую следующий урок переделает на JWT.

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

Соберите сессионный этаж трекера: звено sessions.Sessions("goma", cookie.NewStore из SESSION_SECRET) с Options(MaxAge: 3600, HttpOnly: true), POST /login с c.PostForm и session.Set("user", ...), GET /profile с 401 для гостей, POST /logout с Clear и Save. Добавьте POST /todos с Post-Redirect-Get: AddFlash на успех и ошибку, c.Redirect(http.StatusSeeOther, "/"), чтение Flashes на странице списка из урока про шаблоны. Поставьте отдельную cookie last_seen через c.SetCookie с maxAge на сутки и убедитесь, что она не видна JavaScript-у. Прогоните curl-ом с jar все четыре шага и подделайте значение в jar — сессия должна обнулиться. Оцените, что изменится при переезде на redis.NewStore, и запишите, какие поля вашей сессии нельзя оставлять в cookie-хранилище.

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

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

Чем сессия отличается от просто cookie?

Cookie — носитель: пара имя-значение у клиента. Сессия — слой над ней: словарь с Get/Set и сроками, который либо живёт на сервере (redis), либо подписанным пакетом в той же cookie (cookie.NewStore). Чистая cookie подходит для флагов, сессия — для аутентификации.

Почему после session.Set пользователь всё ещё не залогинен?

Пропущен session.Save(): изменения живут в памяти запроса и попадают в Set-Cookie только по Save. Это самый частый баг темы — тихий, без ошибок. Save нужен и после Delete, Clear и чтения Flashes.

Можно ли спрятать данные сессии от пользователя?

В cookie-хранилище — нет: без blockKey данные видны в base64, с blockKey — шифруются AES, но правило то же: секреты в клиент не кладут. Нужна приватность — redis.NewStore: в cookie остаётся непрозрачный идентификатор, данные на сервере, отзыв — один DEL.

Как удалить cookie?

Повторить Set-Cookie с тем же именем, path и domain и отрицательным maxAge — браузер сотрёт пару. Несовпадающий path не удаляет старую cookie, а создаёт вторую с тем же именем — классическая грабля logout-а.

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

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

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

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

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

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

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

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

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