Go-клиент теряет сессию после входа: подключаем cookiejar
Интеграция успешно вызывает endpoint входа, но следующий запрос снова получает 401. Разработчик использует один http.Client и ожидает поведения браузера. Сам клиент переиспользует транспорт, однако это не означает автоматическое хранение cookies. Для серверной сессии ему нужен CookieJar.
Клиенту нужно хранилище cookies
Сервер передаёт Set-Cookie в ответе. Jar принимает такие значения и при последующих запросах выбирает подходящие cookies для URL с учётом домена, пути и других правил. Когда поле Client.Jar равно nil, клиент не ведёт это состояние автоматически. Поэтому успешный ответ логина и наличие cookie в заголовке ещё не доказывают, что следующий запрос будет аутентифицирован.
Пример запускает локальный сервер с двумя endpoint. /login устанавливает демонстрационную cookie, /private требует именно её. Сначала запрос без Jar получает 401. Затем создаётся отдельный клиент с cookiejar, выполняет вход и получает защищённый ответ. Это проверяет реальный обмен заголовками, а не ручное добавление Cookie в заранее подготовленный запрос.
Установка Path=/ нужна, чтобы cookie от /login подходила и к /private. Для настоящего HTTPS-сервиса сессия обычно также требует Secure и подходящей политики истечения. В локальном HTTP-примере Secure не включён, иначе клиент правильно отказался бы отправлять эту cookie по незашифрованному соединению.
Пройти вход и защищённый запрос
package main
import (
"fmt"
"io"
"net/http"
"net/http/cookiejar"
"net/http/httptest"
"time"
)
func status(client *http.Client, address string) int {
response, err := client.Get(address)
if err != nil {
panic(err)
}
defer response.Body.Close()
if _, err := io.Copy(io.Discard, response.Body); err != nil {
panic(err)
}
return response.StatusCode
}
func main() {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/login" {
http.SetCookie(w, &http.Cookie{Name: "session", Value: "demo", Path: "/", HttpOnly: true})
w.WriteHeader(http.StatusNoContent)
return
}
cookie, err := r.Cookie("session")
if err != nil || cookie.Value != "demo" {
w.WriteHeader(http.StatusUnauthorized)
return
}
io.WriteString(w, "private")
}))
defer server.Close()
plain := &http.Client{Timeout: time.Second}
without := status(plain, server.URL+"/private")
if without != 401 {
panic("missing session accepted")
}
jar, err := cookiejar.New(nil)
if err != nil {
panic(err)
}
client := &http.Client{Jar: jar, Timeout: time.Second}
if status(client, server.URL+"/login") != 204 || status(client, server.URL+"/private") != 200 {
panic("session was not preserved")
}
fmt.Println("without jar:", without, "with jar:", status(client, server.URL+"/private"))
}
Разделяйте клиентов и jars разных пользовательских сессий. Если один общий клиент входит последовательно под разными аккаунтами, сохранённые cookies будут частью общего состояния интеграции. Переиспользование транспорта полезно, но оно не требует объединять аутентификационные данные разных пользователей в один jar.
Изолировать пользовательские сессии
Jar хранит состояние в памяти. При перезапуске программы сессия исчезнет, если приложение отдельно не реализует допустимое сохранение. Для служебной интеграции часто лучше использовать предусмотренный поставщиком токен с ясным сроком действия, чем имитировать интерактивный браузерный вход. Выбор зависит от реального контракта внешнего API.
Параметр PublicSuffixList у cookiejar.Options определяет проверку публичных суффиксов. Для клиента, который ходит по произвольным внешним доменам, используйте подходящую актуальную реализацию списка. В тесте доступен только один локальный host, поэтому nil достаточен для проверки механизма, но это решение не следует переносить автоматически в клиент открытого интернета.
При диагностике посмотрите отдельно Set-Cookie ответа и то, что Jar.Cookies возвращает для конкретного следующего URL. Не выводите значения сессионных cookies в журнал. Проверьте путь, HTTPS, срок истечения и фактический адрес после редиректа: ошибка может находиться в области действия cookie, хотя вход уже прошёл успешно.
Сохраните тест без авторизации и тест с авторизацией. Добавьте раздельные jars двух пользователей, чтобы изменение интеграции не смешало их сессии. Проверка одного статуса 200 после ручного заголовка Cookie не защищает автоматический жизненный цикл хранения.
Продолжить практику
Клиенту важно проверять весь обмен с зависимостью. В курсе микросервисов на Go вы напишете вызовы другой программы, разберёте плохие ответы и контрактные тесты. Откройте программу и продолжите от одного заголовка к проверяемому взаимодействию обработчика и клиента.
Продолжить на практике
- Микросервисы на Go: HTTP, PostgreSQL, gRPC, Kafka и Docker ↗
От работающего Go API — к взаимодействию сервисов, которому можно доверять. Разберитесь, как проектировать контракты, сохранять согласованность данных и восстанавливать обработку после сбоев. HTTP, PostgreSQL, gRPC, Kafka и Docker — через код и наглядные разборы.
