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

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

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

Урок 16 из 25 ~18 мин +20 XP

HttpOnly cookie: сессия без JS

HttpOnly cookie держит jwt-сессию так, что JavaScript её не видит: сервер ставит куку, браузер сам прикладывает её к каждому запросу, а document.cookie остаётся пустым. Разбираем флаги SameSite и Secure, выдачу cookie сессии из Go-хендлера, погашение при выходе и цену, которую схема платит за автоматику — CSRF.

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

После урока про хранение токена логичный вопрос: если localStorage опасен, а память теряется при перезагрузке, почему бы не отдать всю транспортную работу браузеру? Для классического веб-приложения ответ именно такой: httpOnly cookie — старейший рабочий способ носить сессию. Сервер выпускает токен, кладёт его в куку с правильными флагами, и дальше транспорт живёт без единой строки на фронтенде: обновление страницы не роняет сессию, новая вкладка её получает, а скрипт не может прочитать значение, чтобы унести на чужой сервер. Автоматика — одновременно и расплата: кука уезжает с каждым запросом на домен без спроса, чем пользуются CSRF-атаки, незнакомые localStorage-схеме. В этом уроке соберём cookie сессии go-хендлером с тремя флагами, посмотрим, что меняется в серверном middleware, и правильно потушим сессию при выходе. Разговор опирается на материал уроков с шестого по пятнадцатый: транспорт здесь новый, а криптография — та же.

Механика из трёх участников. Сервер на логине отвечает заголовком Set-Cookie со значением токена и атрибутами HttpOnly, Secure, SameSite — браузер сохраняет куку, привязав её к домену. Дальше браузер сам прикладывает её к каждому запросу на этот домен заголовком Cookie — и к AJAX, и к переходам по ссылкам, без какого-либо кода на клиенте. HttpOnly — единственный атрибут, про который браузер молчит для JavaScript: кука хранится и отправляется, но в document.cookie не видна. Серверной стороне всё равно, каким транспортом приехал токен: middleware достаёт строку вызовом r.Cookie и отдаёт её в тот же ParseWithClaims из шестого урока. Меняется транспорт — кука вместо заголовка Authorization из урока про middleware — а проверка подписи, exp и ролей остаётся ровно той же. JWT внутри куки — всё тот же токен, просто в другой обёртке. Отсюда практический вывод: перевод сервиса на куки не трогает выпуск токенов и не ослабляет проверку — меняется только то, откуда middleware читает строку, и это ограничивает правки одним файлом.

Выдача cookie сессии go-хендлером

GO
func handleLogin(w http.ResponseWriter, r *http.Request) {
        var in Credentials
        if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
                http.Error(w, "битый JSON", http.StatusBadRequest)
                return
        }
        if bcrypt.CompareHashAndPassword(users.Hash(in.Email), []byte(in.Password)) != nil {
                http.Error(w, "неверные данные входа", http.StatusUnauthorized)
                return
        }
        token, err := IssueAccess(in.Email, "user")
        if err != nil {
                http.Error(w, "сервер не смог выпустить токен", http.StatusInternalServerError)
                return
        }
        http.SetCookie(w, &http.Cookie{
                Name:     "access",
                Value:    token,
                Path:     "/",
                HttpOnly: true,
                Secure:   true,
                SameSite: http.SameSiteLaxMode,
                MaxAge:   int((15 * time.Minute).Seconds()),
        })
        w.WriteHeader(http.StatusOK)
}

Сессия собирается из трёх частей: bcrypt-сравнение пароля из девятого урока, выпуск access-токена и http.SetCookie. Флаги идут связкой: HttpOnly закрывает значение от document.cookie, Secure запрещает передачу по открытому HTTP, SameSite Lax держит куку дома при кросс-сайтовых POST. MaxAge совпадает со сроком access-токена: куке нечего делать дольше своей сессии. Тела в ответе можно не писать — фронту достаточно статуса 200.

Флаги SameSite и Secure: что закрывает каждый#

Secure разрешает отправку только по HTTPS: кука, случайно поставленная на открытый порт, не уедет по сети в открытом виде. SameSite управляет кросс-сайтовыми запросами: Lax пускает куку в безопасных переходах верхнего уровня — клик по ссылке, — но не в кросс-сайтовых POST-формах, которыми и живёт CSRF; Strict не отправляет её с чужого сайта вовсе; None отправляет везде и требует Secure, без него браузеры куку отбросят. Path сужает область: кука с Path=/api/auth/refresh приедет только на один эндпоинт, а не на весь API. Для access-куки обычного сайта рабочая связка — HttpOnly, Secure, Lax и Path=/; refresh-куке вместо корня берут узкий маршрут. SameSite None оставьте для случаев, когда фронтенд и API физически на разных доменах: тогда CSRF-токен становится обязательным — подробно в уроке про CSRF. Проверяйте связку в браузере, а не на словах: вкладка Network показывает и Set-Cookie с флагами, и то, какие куки реально уехали с запросом — две минуты в devtools заменяют час чтения спеки.

АтрибутЧто делаетДефолт для access-сессии
HttpOnlyскрывает значение от document.cookie и скриптоввключён всегда
Secureотправка только по HTTPSвключён всегда
SameSiteполитика кросс-сайтовых запросовLax
Pathмаршрут, на который кука уезжает/ для access, узкий для refresh
MaxAgeсрок жизни куки в секундахравен сроку токена

Серверная правка компактная. Вместо разбора заголовка — чтение куки: c, err := r.Cookie("access"), при ошибке — 401 без стыда, ровно как с пустым Authorization. Дальше строка идёт в привычный ParseWithClaims с WithValidMethods, и всё построенное поверх — контекст с пользователем, RequireRole из одиннадцатого урока — работает без изменений. Полезный компромисс на переходный период — дуал: сначала кука, потом заголовок Bearer; один эндпоинт обслуживает браузер, мобильное приложение и curl в скриптах. Дуал я считаю временным решением: два транспорта — вдвое больше сочетаний для тестов и поверхности для ошибок, и как только аудитория клиента определится, второй вариант стоит убрать. Полная серверная сборка на chi, где такое middleware встраивается в цепочку, разобрана в курсе по chi; на gin механика та же — c.Cookie со схожими флагами, детали в уроке про JWT в gin. Проверка после правки проста: запрос без куки обязан вернуть 401, запрос с кукой — пройти до хендлера; оба случая по отдельности гоняются curl-ом с файлом куки.

У схемы есть тихие преимущества, которые всплывают в эксплуатации. Сессия переживает перезагрузку без вызова refresh: access в куке живёт свои пятнадцать минут и обновляется на лету. Усиление безопасности не требует миграции фронтенда — флаги ставит сервер. Бюджет XSS ломается: скрипт, попавший на страницу, не может ни прочитать токен, ни вынести его за пределы браузера жертвы — он может только слать запросы изнутри, и то ограниченно при правильном SameSite. Цена — CSRF и чуть более хитрая отладка: из консоли куку не прочитать, и новичкам первое время это кажется поломкой. Для SPA с разделёнными доменами гибрид из четырнадцатого урока часто удобнее; для монолита с шаблонами или сайта с классическими страницами httponly cookie jwt — дефолт, от которого отступают по конкретной причине, а не по вкусу. Мобильные клиенты в эту схему не вписываются — они пользуются заголовком Authorization, и это не дефект, а повод держать дуал, пока у вас смешанная аудитория.

Выход: MaxAge -1 и denylist

GO
func handleLogout(w http.ResponseWriter, r *http.Request) {
        if c, err := r.Cookie("access"); err == nil {
                _ = Revoke(c.Value)
        }
        http.SetCookie(w, &http.Cookie{
                Name:     "access",
                Value:    "",
                Path:     "/",
                HttpOnly: true,
                Secure:   true,
                SameSite: http.SameSiteLaxMode,
                MaxAge:   -1,
        })
        w.WriteHeader(http.StatusNoContent)
}

Выход — это два действия, а не одно. MaxAge меньше нуля приказывает браузеру немедленно удалить куку, но сам jwt остаётся криптографически годным до exp: его гасит запись в denylist по jti, собранная в уроке про отзыв. Обнулять Value не обязательно, но в логах пустая кука читается проще. Статус 204 сообщает фронту, что повторять запрос бессмысленно.

Проверка: HttpOnly и скрипт

Что прочитает скрипт в document.cookie, если access-кука выставлена с HttpOnly?

Проверка: SameSite и кросс-сайтовый POST

Пользователь залогинен, access-кука с SameSite Lax. Чужой сайт отправляет POST-форму на ваш /api/transfer. Что увидит сервер?

Strict против ссылки из письма

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

// кука выставлена так:
// Set-Cookie: access=eyJ...; HttpOnly; Secure; SameSite=Strict

// пользователь читает письмо в чужом веб-клиенте
// и кликает ссылку на https://api.example.com/api/inbox
// что получит сервер?

Восстановите выставление cookie

Допишите флаги защищённой access-куки: имя, два булевых флага и политику SameSite.

http.SetCookie(w, &http.Cookie{ Name: "", Value: token, Path: "/", HttpOnly: , Secure: , SameSite: http., MaxAge: int((15 * time.Minute).Seconds()), })

Соберите выпуск cookie-сессии

Расставьте строки хендлера в правильном порядке: от выпуска токена до закрывающей скобки SetCookie.

Строки

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

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

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

Термин

HttpOnly

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

Атрибут cookie: значение скрыто от document.cookie и любого скрипта. Браузер хранит и отправляет куку сам; XSS не может прочитать токен.

1 / 5

Cookie-сессия закрывает клиентскую половину истории; серверную вы уже умеете: middleware из десятого урока проверяет строку так же, как и раньше, а роли ложатся сверху без правок. Когда сессия нужна не человеку, а соседнему сервису, транспорт меняется — об этом следующий урок про сервисные токены. Второй слой защиты куки от чужих форм — CSRF-токен — разобран в уроке двадцать первом. Пароль, который проверяется перед выпуском куки, хешируется по правилам урока про bcrypt. Повторить сценарий проще всего на локальном Go: два хендлера и curl с сохранением куки в файл дают полный цикл за пару минут. Карта всех тем — на странице курса jwt-go; cookie-механика в другом фреймворке собрана в уроке про cookie и сессии gin.

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

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

Вывод

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

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

  • HttpOnly cookie jwt — сессия без JS: сервер ставит куку, браузер возит её сам, скрипт значение не читает.
  • Связка флагов: HttpOnly против XSS-кражи, Secure против открытого HTTP, SameSite Lax против кросс-сайтового POST.
  • Серверная правка минимальна: r.Cookie вместо заголовка, дальше тот же ParseWithClaims и RequireRole.
  • Refresh-куке — узкий Path вроде /api/auth/refresh, чтобы кука не прилетала на весь API.
  • Logout — два действия: кука с MaxAge -1 и запись в denylist по jti.
  • Цена автоматики — CSRF: SameSite режет вектор первым слоем, CSRF-токен вторым.

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

Понадобится локальный Go: соберите хендлеры login и logout из урока, выпустите access-куку с тремя флагами и проверьте в devtools — вкладка Application должна показать HttpOnly, а document.cookie остаться пустым. Затем отправьте POST-форму на защищённый эндпоинт со страницы на другом порту и посмотрите в логах, что SameSite Lax оставил куку дома. Добавьте к logout запись в denylist и убедитесь, что сохранённая до выхода копия токена больше не проходит — это и есть критерий, что выход настоящий, а не косметический. Заодно запишите, какой из трёх флагов вы сняли бы последним, если бы пришлось; правильный ответ — никакой.

Личные заметкиПусто
Хранятся только в вашем браузере (JWT · HttpOnly cookie: сессия без JS)

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

JWT в httpOnly cookie — не теряем ли statelessness?

Нет: состояние лежит в подписанном токене, сервер по-прежнему не хранит ничего между запросами. Кука — транспорт, а не серверное хранилище: подпись и exp по-прежнему несут всю проверку. Сравнение классических сессий и JWT по числу ходов и точкам отказа есть в уроке про хранение токена.

Как отлаживать HttpOnly куку?

Вкладка Application в devtools показывает все куки, включая HttpOnly, со значениями и флагами — снимать флаг ради отладки не нужно. На сервере удобно логировать факт присутствия куки и срок её жизни, но не само значение токена.

Что делать с мобильными приложениями?

Мобильные клиенты обычно ходят с заголовком Authorization: Bearer из защищённого хранилища платформы — Keychain у iOS, EncryptedSharedPreferences у Android. Поэтому дуал кука плюс заголовок оправдан на переходный период или как постоянная схема для смешанной аудитории.

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

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

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

Telegram25 уроков~12 чновичок25 уроков уже доступны

Telegram-боты на Go: от эха до полноценного сервиса

Учимся писать Telegram-ботов на Go: токен BotFather, команды, клавиатуры, состояния диалога, медиа, рассылки, webhook и деплой. 25 уроков с кодом.

  • Создавать бота через BotFather и хранить токен безопасно
  • Получать обновления через long polling и webhook
  • Отвечать сообщениями, командами и клавиатурами
  • Строить многошаговые диалоги с сохранением состояния