Содержание урока · 13 разделов
- gin cookie: HTTP, который ничего не помнит
- Анатомия c.SetCookie: семь аргументов и http.Cookie
- Безопасность cookie: HttpOnly, Secure, SameSite
- сессии gin: словарь на сервере, ключ в cookie
- Что внутри cookie-хранилища: подпись, а не шифрование
- Хранилища сессий: cookie, redis, memstore
- Справочник: cookie и сессии в gin
- Флеш-сообщения: AddFlash и сценарии после редиректа
- Отладка: curl с cookie jar и DevTools
- Семь правил cookie и сессий
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
gin cookie: HTTP, который ничего не помнит#
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: счётчик визитов
GOpackage 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: семь аргументов и http.Cookie#
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 получит браузер?
Безопасность cookie: HttpOnly, Secure, SameSite#
Три флага — три линии обороны, и включать их нужно осознанно, а не «когда-нибудь в проде». 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 не кладут пароли, токены карт и персональные данные — слово клиента читается как открытая книга, вплоть до того, что пользователь вправе его отредактировать.
сессии gin: словарь на сервере, ключ в 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
GOstore := 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-хранилища: подпись, а не шифрование#
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"})
})Хранилища сессий: cookie, redis, memstore#
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 у пакетов один и тот же.
Справочник: cookie и сессии в gin#
| Инструмент | Что делает | Когда брать |
|---|---|---|
| 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 плюс HMAC | stateless, немного нечувствительных данных |
| 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 с флешем
GOr.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") между запросами?
Отладка: curl с cookie jar и DevTools#
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
Допишите обработку формы: достать сессию, положить сообщение-флеш, опубликовать изменения и редиректнуть на список.
Соберите путь сессии от логина до профиля
Cookie-хранилище, POST /login с username=om, затем GET /profile: разложите события по порядку.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: cookie и сессии
Семь правил cookie и сессий#
- 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.
Соберите трекер и пройдите весь маршрут без браузера: curl -c jar.txt -X POST -d "username=om" localhost:8080/login — 200; curl -b jar.txt localhost:8080/profile — профиль; curl -b jar.txt -X POST localhost:8080/logout — выход; повторный profile — 401. Подделайте значение в jar.txt руками — HMAC не сойдётся и сессия обнулится: вы только что проверили подпись securecookie. В браузерной песочнице сайта сетевой код не исполняется, поэтому runner-ов в курсе нет — интерактивы здесь quiz, predict, fill, order и флешкарты, а примеры живут в локальном go run, как и весь курс gin. Дальше — 15-jwt: аутентификация JWT — подпись, которую сервер проверяет без хранилища сессий, гвард с Authorization Bearer и refresh-токены.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +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-хранилище.
Личные заметкиПусто
Вопросы и ответы по уроку
Чем сессия отличается от просто 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-а.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Echo: минималистичный быстрый фреймворк
Курс Echo для Go: маршруты, middleware, валидация, JWT, SSE, тестирование и деплой. 25 уроков с примерами и итоговым API-проектом.
- Строить REST API на Echo
- Использовать типизированные запросы/ответы (Request/Response)
- Управлять ошибками через echo.HTTPError
- Писать middleware, включая JWT-аутентификацию