Содержание урока · 8 разделов
Начнём с простой сцены. Вы открыли интернет-магазин, залогинились, перешли в корзину — и сайт вас узнал. Со стороны кажется, что магии нет, но протокол HTTP не хранит состояния: каждый запрос приходит на сервер как незнакомец, без памяти о предыдущих. Кто-то обязан носить удостоверение личности с каждым обращением, и от того, как устроено это удостоверение, зависит половина архитектуры сервиса. Исторически сложилось два подхода. Первый — сессии: сервер выдаёт клиенту короткий случайный идентификатор в cookie и держит у себя таблицу «идентификатор → пользователь». Второй — токены: сервер выдаёт клиенту подписанный документ, в котором написано, кто этот клиент и до какого времени документ действителен, а хранить ничего не хранит. В свежих Go-сервисах, особенно в API и микросервисах, чаще встречается второй путь, и самым заметным его представителем стал JWT. В этом уроке пройдём обе схемы честно, чтобы ваш выбор был осознанным, а не скопированным из первого попавшегося туториала.
Аутентификация и авторизация: две разные проверки#
Эти два слова путают даже опытные разработчики, а разница между ними — половина вопросов собеседований по аутентификации golang. Аутентификация отвечает на вопрос «кто ты»: клиент предъявляет токен, пароль или отпечаток, сервер убеждается, что личность настоящая. Авторизация отвечает на вопрос «что тебе можно»: личность уже установлена, и сервер сверяет её права с запрошенным действием. Пользователь с ролью user, запрашивающий DELETE /admin/users, аутентифицирован идеально — и всё равно получит отказ, потому что прав у него нет. В HTTP для этого заведены два разных статуса: 401 Unauthorized — «не понял, кто ты, предъяви документ», и 403 Forbidden — «знаю, кто ты, но нельзя». Привычка отвечать 401 на всё подряд кажется мелочью, а стоит дорого: клиентские библиотеки по 401 запускают повторный вход, и пользователь с достаточными правами окажется выброшен на экран логина без причины. В середине курса мы соберём middleware, который проверяет обе вещи по очереди: сначала подпись и срок токена, потом роль из claims.
JWT-токены: что решает подпись и что не решает#
JWT-токены переворачивают схему сессий: данные о пользователе и срок годности подписываются сервером и уезжают к клиенту целиком. Сервер хранит единственную общую строку — секрет подписи; каждая реплика кластера проверяет токен самостоятельно, без похода в общее хранилище. Отсюда главное преимущество: горизонтальное масштабирование, при котором не нужно синхронизировать сессии между узлами, и рестарт процесса, который ничего не забывает. Подпись гарантирует целостность: изменить содержимое токена, не пересчитав подпись, нельзя, а пересчитать её без секрета — невозможно. Но у медали есть оборот, о котором узнают слишком поздно. Подпись не шифрует: любой, кто увидел токен — прокси, логи, расширение браузера, — прочтёт содержимое. Отзыв встроен не будет: токен валиден до exp, и «выкинуть» его досрочно по желанию сервера нельзя — для этого строят отдельные механизмы вроде чёрных списков, до которых курс доберётся в середине. И наконец, JWT не отменяет проверку прав: роль, записанная в токене, — утверждение на момент выпуска, а не живое состояние.
Настоящий токен и его разбор без секретов
GOtoken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsInJvbGUiOiJhZG1pbiJ9.-24izfUYTb3Z6pUvdW9g8q7V1MRiF5aJAtYiW-ZTKxw"
parts := strings.Split(token, ".")
fmt.Println("частей:", len(parts))
header, _ := base64.RawURLEncoding.DecodeString(parts[0])
payload, _ := base64.RawURLEncoding.DecodeString(parts[1])
fmt.Println(string(header)) // {"alg":"HS256","typ":"JWT"}
fmt.Println(string(payload)) // {"sub":"42","role":"admin"}Настоящий HS256-токен выглядит именно так: три блока, разделённые точками. strings.Split режет строку, а base64.RawURLEncoding из стандартной библиотеки превращает блоки в читаемый JSON — ни секрет, ни внешняя библиотека для этого не нужны. Подпись в третьей части пока просто показана: как она считается и почему её нельзя обмануть, разберём в уроке про устройство токена.
Токен, выпущенный без поля exp, остаётся валидным вечно: подпись у него настоящая, а срока годности нет. Утёкший такой токен нельзя вывести из строя штатными средствами — JWT не знает слова logout. Правило жёсткое: каждый выпускаемый токен получает ExpiresAt, access-токены живут минуты, а не недели; долгие полномочия отдайте refresh-токену с отдельной судьбой, о нём будет отдельный урок.
Как бы ни была устроена схема, серверная проверка укладывается в пять шагов. Достать заголовок Authorization и убедиться, что он начинается со слова Bearer. Отрезать это слово и оставить строку токена. Проверить подпись — пересчитать её своим секретом и сравнить. Проверить срок: exp не должен остаться в прошлом, а nbf — в будущем. Достать из payload поля пользователя и положить их в контекст запроса, чтобы хендлеры не думали о токенах вовсе. Каждый шаг умеет отказывать по-своему: нет заголовка, кривая строка, чужая подпись, истёкший срок — и честный сервис отвечает на эти случаи по-разному, чему будет посвящён отдельный урок. Все пять шагов в живом сервисе прячет middleware: хендлер получает уже готового пользователя и занимается бизнес-логикой. Именно такой middleware мы соберём на chi и Gin в середине курса, а сейчас важно уложить в голове порядок: подпись проверяется раньше содержимого, а содержимое — раньше прав.
Cookie-сессии против JWT: когда что брать#
Схема JWT — не универсальный ответ, и честнее сравнить её с классикой. Cookie-сессии проще устроены: короткий идентификатор, хранилище на сервере, мгновенный отзыв — удалил запись, и пользователь разлогинен. Для монолитного сайта с шаблонами и формами я бы начал именно с них: меньше криптографии, меньше вопросов с хранением токена на клиенте. Отдельное хранилище сессий, например в Redis, поднимается за один вечер — на сайте есть курс про Redis в Go, где это разобрано по шагам. JWT окупается там, где клиентов много и они разные: мобильное приложение без cookie jar, несколько микросервисов, которым лень ходить в общий store за каждой проверкой, публичный API для сторонних разработчиков. Второй аргумент — нагрузка: проверка подписи дешевле, чем сетевой поход в хранилище на каждый запрос. Моё рабочее правило: один монолит и веб-интерфейс — cookie-сессии; распределённая система или API — JWT. Если сомневаетесь, посмотрите, как обе схемы живут во фреймворках: в курсе Gin есть уроки и про cookie-сессии, и про JWT — сравните на знакомом коде.
Две схемы аутентификации в одном кадре#
| Критерий | Cookie-сессии | JWT |
|---|---|---|
| Где живёт состояние | в хранилище сервера | в самом токене |
| Масштабирование на реплики | общий store, синхронизация | каждая реплика проверяет подпись сама |
| Отзыв до истечения | удалить запись — мгновенно | нужен отдельный denylist |
| Мобильные и CLI-клиенты | имитация cookie jar | один заголовок Authorization |
| Размер на каждый запрос | короткий идентификатор | сотни байт |
В жизненном цикле токена три роли, и полезно называть их по именам. Issuer — тот, кто выпускает: сервис логина, который проверил пароль, собрал claims и подписал токен. Проверяющий — любой сервис, к которому клиент придёт с этим токеном: он не знает пароля, не видел таблицы пользователей, но верит подписи, потому что умеет проверять её секретом или публичным ключом issuer-а. Носитель — сам клиент: браузер, мобильное приложение, другой бэкенд; его задача — хранить токен и прикладывать к запросам, не показывая никому лишнему. Разделение ролей объясняет, почему JWT так любят в микросервисах: auth-сервис выпускает, а остальные проверяют без единого похода к нему. Там же кроется ограничение, о котором стоит помнить заранее: чем дольше живёт токен, тем дольше он остаётся снимком момента выпуска. Поэтому короткий access-токен и refresh-схема появятся в курсе не как мода, а как следствие этого устройства.
Проверка: 401 или 403
Клиент предъявил валидный токен с ролью user и запросил DELETE /admin/users. Какой статус вернёт корректный сервис?
Проверка: что хранит сервер
Что хранит сервер, чтобы проверять подписи HS256-токенов сразу у всех пользователей?
Заглядывание вперёд: выпуск токена в пять строк
GOsecret := os.Getenv("JWT_SECRET")
t := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": "42",
"role": "admin",
"exp": jwt.NewNumericDate(time.Now().Add(15 * time.Minute)),
})
signed, err := t.SignedString([]byte(secret))
if err != nil {
log.Fatal(err)
}
fmt.Println(signed)Это заглядывание в урок про установку библиотеки: jwt.NewWithClaims собирает заготовку токена из алгоритма и набора claims, SignedString подписывает её секретом из переменной окружения и отдаёт готовую строку. exp на пятнадцать минут — привычный срок жизни access-токена, запомните его. Полный разбор каждой строки — через два урока.
Что вернёт гвард на пустой заголовок?
Прочитайте код и выберите, что он выведет:
func guard(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
w.WriteHeader(http.StatusUnauthorized)
return
}
if !allowed(r) {
w.WriteHeader(http.StatusForbidden)
return
}
next(w, r)
}
}
// запрос: GET /profile без заголовка AuthorizationДва отказа — два статуса
Допишите статусы для двух отказов: клиент без токена и клиент с недостаточными правами.
Соберите main() с двумя защищёнными маршрутами
Расставьте строки минимального API так, чтобы оба маршрута подключились через один middleware-гвард guard().
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карта курса jwt go: от первого токена до auth-сервиса#
Курс jwt go построен по нарастающей, и вводный урок — единственный без боевого кода. Дальше: устройство токена по слоям — header, payload, signature — в следующем уроке; установка golang-jwt v5 и первый токен в пять строк; claims со стандартными и своими полями; HMAC и HS256 с секретом и правилами его хранения. После этого серверная часть: проверка токена через ParseWithClaims, ошибки и честные ответы клиенту, refresh-токены с ротацией. Затем инфраструктура: middleware аутентификации, роли, RSA с асимметричными подписями, отзыв токенов и cookie-сессии. Финал — итоговый auth-сервис с регистрацией, логином и ролями. Одна честная оговорка заранее: это библиотечный курс, и его код потребует локального Go — браузерная песочница сайта не умеет ни внешние пакеты, ни сеть. Установка с go.dev занимает минут десять, а каждый урок снабжён домашним заданием под локальный запуск. Полный список тем всегда под рукой на странице курса JWT-аутентификации.
Карточки: базовые понятия
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- HTTP не помнит клиентов: удостоверение личности едет с каждым запросом — cookie-сессией или токеном.
- Аутентификация отвечает на «кто ты» (401), авторизация — на «что тебе можно» (403); это две разные проверки.
- JWT переносит состояние в сам токен: сервер хранит один секрет, реплики не синхронизируют сессии.
- Подпись гарантирует целостность, но не конфиденциальность: payload читает каждый, кто увидел токен.
- Токен без exp — вечный ключ; короткий срок жизни обязателен, долгие полномочия — у refresh-токена.
- Cookie-сессии проще для монолита с веб-интерфейсом, JWT окупается в API и распределённых системах.
- Серверная проверка — пять шагов от заголовка Authorization до пользователя в контексте запроса.
- В схеме три роли: issuer выпускает, проверяющий сверяет подпись, носитель бережёт токен и прикладывает к запросам.
Домашнее задание
Без кода: возьмите любой сервис, которым пользуетесь, и опишите его аутентификацию по схеме урока — где живёт состояние, как выглядит удостоверение, что происходит при отзыве. Затем откройте DevTools, сеть одного запроса к API, и найдите заголовок Authorization: если там Bearer и длинная строка с двумя точками — перед вами JWT, разложите его на три части глазами и попробуйте назвать каждую роль из урока: кто выпустил, кто проверит, кто носит.
Личные заметкиПусто
Вопросы и ответы по уроку
JWT — это шифрование?
Нет. JWT — подпись: она гарантирует, что содержимое не изменено после выпуска, но не прячет его. Любой, кто увидел токен, декодирует payload обычным base64url. Для сокрытия данных существует отдельный стандарт JWE, но в веб-аутентификации он встречается редко.
Почему нельзя класть пароль в JWT?
Payload токена кодируется, а не шифруется, и живёт у клиента до истечения exp. Пароль, персональные данные и любые секреты в claims — типовая утечка; в токен кладут только идентификатор, роль и служебные поля.
Можно ли обойтись без JWT вообще?
Да. Cookie-сессии с хранилищем на сервере решают ту же задачу и проще в отзыве. JWT выигрывает, когда клиентов много, они разнородны, или проверки должны идти без общего хранилища — например, в микросервисах и публичных API.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Telegram-боты на Go: от эха до полноценного сервиса
Учимся писать Telegram-ботов на Go: токен BotFather, команды, клавиатуры, состояния диалога, медиа, рассылки, webhook и деплой. 25 уроков с кодом.
- Создавать бота через BotFather и хранить токен безопасно
- Получать обновления через long polling и webhook
- Отвечать сообщениями, командами и клавиатурами
- Строить многошаговые диалоги с сохранением состояния