Ситуация: до релиза два дня, auth-код готов, и код-ревью превращается в бесконечный спор — пятнадцать минут или час на access, хранить ли секрет в переменных окружения или в менеджере секретов, нужен ли denylist на старте. Каждый пункт по отдельности где-то уже обсуждался в курсе, но по памяти спорят плохо. Чек-лист — это форма, которая делает jwt безопасность прогоняемой: список утверждений с дефолтом и ссылкой на урок, где лежит обоснование. Дефолт — не догма: отклонение допустимо, но оно обязано быть сознательным и записанным рядом с кодом. Практика, которая работает: чек-лист живёт в репозитории рядом с auth-пакетом, а новый пункт добавляется только через инцидент или инцидент чужой, прочитанный в чьём-то постмортеме. Ниже — восемь пунктов, которые покрывают подавляющее большинство найденных на ревью проблем, и три места, где чек-лист молчит, если вы забыли про него при проектировании.
jwt безопасность: сроки жизни и refresh#
Первый пункт — сроки. Access живёт пятнадцать минут: окно достаточно короткое, чтобы утечка быстро теряла силу, и достаточно длинное, чтобы фронтенд не долбил /refresh на каждый клик. Границы честные: для подтверждения платежа и смены пароля access ужимают до пяти минут — операция должна закрыться в одном окне; для внутренних дашбордов с редкой активностью час допустим, но тогда обязателен refresh при первом же запросе после простоя. Refresh живёт месяц и обязан ротироваться на каждом обновлении — схема из восьмого урока: без ротации один украденный refresh превращается в вечную сессию. MaxAge куки равен сроку токена — куке нечего делать дольше, чем её содержимому. И помните про часы: golang-jwt сверяет exp по серверному времени, а между машинами бывает расхождение, поэтому в средах с дисциплиной синхронизации времени полезен leeway в несколько секунд: по умолчанию golang-jwt сверяет exp строго, без всякого прощения, и расхождение часов между машинами ловится как просрочка.
jwt лучшие практики: секрет и ключи#
Второй пункт — происхождение секрета. Минимум 32 байта криптографической случайности, сгенерированные один раз и живущие в переменной окружения или менеджере секретов — никогда в коде и никогда не придуманные вручную: человеческие «секреты» перебираются словарём за минуты. Для HS256 32 байта — это 256 бит энтропии, ровно размер ключа SHA-256; меньше — перебор становится теоретически осмысленным, больше — бессмысленно. Третий пункт — алгоритмы: белый список в WithValidMethods, без которого открывается весь класс атак из двадцать третьего урока. Четвёртый — ротация: с kid в заголовке сервис умеет проверять двумя секретами одновременно и переживает смену, не выгоняя пользователей. Пятый — свежая библиотека: dgrijalva/jwt-go заморожен, актуальный форк golang-jwt v5 из третьего урока закрывает накопленные CVE; устаревшая зависимость — это не риск, а расписка, что уязвимости достанутся вам.
Секрет из env с fail fast
GOfunc loadSecret() []byte {
s := os.Getenv("JWT_SECRET")
if len(s) < 32 {
// Секрет слабый или не задан: падаем на старте,
// а не подписываем токены перебираемым значением
log.Fatal("JWT_SECRET: минимум 32 байта, из окружения")
}
return []byte(s)
}Fail fast — принцип, ради которого пункт существует: сервис, поднявшийся со слабым секретом, выглядит рабочим до первого инцидента. Генерация значения — раз и навсегда: openssl rand -base64 32 в терминале, результат сразу в окружение. Ровно тот же тест длины стоит прогонять в CI на файлах окружения тестовых стендов — там слабые значения живут чаще всего.
Чек-лист перед релизом#
- Access exp 15 минут, для платёжных операций — 5; refresh 30 дней с ротацией на каждом обновлении.
- Секрет — 32 байта случайности из env или менеджера секретов; в коде и git его нет.
- WithValidMethods с явным списком алгоритмов: HS256 или RS256/ES256, но не всё сразу без переходного плана.
- kid в токенах и поддержка двух ключей при ротации; старый ключ отзывается только после ухода последних токенов.
- Транспорт: httpOnly, Secure, SameSite для cookie-схем; Bearer в памяти для мобильных и SPA без куки.
- CSRF-токен при cookie-сессиях из двадцать первого урока; правило «меняет состояние — только POST».
- Отзыв: jti в каждом токене, denylist с TTL до exp, отзыв семьи при повторе ротированного refresh.
- bcrypt с cost 10-12 для паролей и одинаковые сообщения на неверный email и неверный пароль.
- Аудит: лог выпуска, логина, отказа и отзыва с user id; алерты на всплески 401 и на повтор старого refresh.
- Тесты на 401/403 из двадцатого урока в CI: чужая подпись, просрочка, роль user на admin-маршруте.
Хранение и транспорт токена#
Транспорт и хранение — шестой и седьмой пункты, и они почти целиком переписываются ссылками на материал курса. Не localStorage: XSS выносит оттуда токен без следов, сравнение схем хранения разобрано в четырнадцатом уроке. Cookie-схема — только с HttpOnly, Secure и SameSite из шестнадцатого, плюс CSRF-токен из двадцать первого, потому что SameSite — слой браузера, а не гарантия. В payload не кладут пароли, платёжные детали и лишние персональные данные: payload читается всеми, кто видит токен, и это свойство, а не дефект — из него следует дисциплина содержимого. Восьмой пункт — bcrypt из девятого урока с честной стоимостью: cost 12 на типичном сервере — порядка сотни миллисекунд, и это цена, которую логин обязан платить; экономия на bcrypt экономит обычно на чьём-то аккаунте. Отдельный подпункт — длина пароля при регистрации: минимум восемь символов при отсутствии других ограничений состава, потому что длинный пароль с bcrypt-стоимостью и так перебирается нереально.
Аудит и эксплуатация#
Аудит — пункт, который отличает систему, переживающую инциденты, от системы, узнающей о них от пользователей. Минимальный набор событий: выпуск пары, отказ проверки с причиной из седьмого урока, ротация, отзыв и повтор старого refresh — последний сигналит о краже, и его алерт из девятнадцатого урока должен звонить ночью, а не лежать в логе. Всплески 401 на логине — это либо релиз с битым фронтендом, либо перебор: в обоих случаях разница видна по адресам и агентам, а ограничение частоты на /login из rate limiter на Redis превращает перебор в шум. И последнее: версия библиотеки проверяется тем же автоматическим ритмом dependabot, что и остальное, — jwt-зависимость в списке устаревших должна вызывать ту же реакцию, что и CVE в HTTP-фреймворке.
Классический сценарий: .env закоммичен «временно» на старте проекта и прожил в истории два года. Ротация пароля или токена не помогает — секрет остался в каждом клоне и в каждом бэкапе репозитория. Лечение только одно: смена секрета с kid-переходом и уборка из истории; профилактика — .gitignore с первого дня, gitleaks или trufflehog в CI и запрет на файлы окружения в репозитории. Отдельно проверяйте тестовые секреты: хардкод в тестах допустим, но он обязан быть заведомо тестовым значением, а не «тем же самым, что в проде, для удобства».
Чек-лист: пункт и дефолт#
| Пункт | Дефолт | Отступление допустимо, когда |
|---|---|---|
| exp access | 15 минут | платёжные операции — 5 минут; дашборды — до часа с refresh после простоя |
| refresh | 30 дней с ротацией | короче для высокой чувствительности; дольше только с отзывом семьи |
| секрет HS256 | 32 байта из env | никогда — длину и источник не ужимают |
| алгоритмы | белый список одним методом | два семейства — только переходный период с двумя валидаторами |
| cookie-флаги | HttpOnly + Secure + Lax | Strict для refresh; None только для кросс-доменных API с CSRF-токеном |
| отзыв | jti + denylist, TTL до exp | без denylist — только exp до 15 минут и обязательная ротация |
| пароли | bcrypt cost 10-12 | cost выше на отдельной машине выпуска токенов |
| аудит | лог выпуска, отказа, отзыва | алерты обязательны при любом объёме |
Понадобится локальный Go: напишите скрипт, который читает JWT_SECRET, проверяет длину и выпускает токен, — и гоняйте его на каждом стенде. Браузерная песочница курса из fmt, strings, strconv, math, sort, errors и time умеет проверить длину строки и срезы — саму логику валидации секрета можно отработать прямо в ней, а env и curl остаются локальными. Остальные пункты проверяются глазами за полчаса, если держать таблицу из этого урока под рукой.
Проверка: слабый секрет в env
JWT_SECRET не задан или короче 32 байт. Как поступает сервис с fail fast?
Проверка: зачем kid
Что даёт kid в заголовке токена при ротации секрета?
Проверка длины секрета
Прочитайте код и выберите, что он выведет:
s := os.Getenv("JWT_SECRET") // переменная не задана
if len(s) < 32 {
log.Fatal("JWT_SECRET: минимум 32 байта, из окружения")
}
// ...далее — подъём роутера и выпуск первого токенаСоберите проверку конфигурации
Допишите чтение секрета из окружения, порог длины и белый список алгоритмов.
Соберите ротацию секрета
Расставьте шаги плановой ротации HS256-секрета в правильном порядке.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: чек-лист JWT
Чек-лист из восьми пунктов — рабочий минимум, и его ценность в прогоняемости: перед релизом проходит пять минут, а не спор на час. Дальше — финал: итоговый проект собирает курс в один auth-сервис, где каждый пункт списка превращается в строку кода. Ссылки на обоснования: сроки и ротация — восьмой урок, секрет и HS256 — пятый, alg-атаки и белый список — двадцать третий, cookie-флаги — шестнадцатый, CSRF — двадцать первый, тесты — двадцатый, а хранение сессий как альтернатива токенам — урок про сессии в Redis. Карта всех тем — на странице jwt-go.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Чек-лист — форма, а не память: восемь пунктов с дефолтами и границами, прогоняемые перед каждым релизом.
- Сроки: access 15 минут, платёжные операции 5, refresh 30 дней с обязательной ротацией.
- Секрет — 32 байта случайности из env или менеджера, fail fast на старте, kid для ротации без выгона пользователей.
- Белый список алгоритмов и свежая библиотека закрывают класс атак на заголовок.
- Транспорт: HttpOnly, Secure, SameSite плюс CSRF-токен; в payload нет ничего секретного.
- Аудит — выпуск, отказ, ротация, отзыв и повтор старого refresh с алертом, который звонит ночью.
Домашнее задание
Прогоните чек-лист по собственному сервису и выпишите для каждого пункта: выполняется, не выполняется или отступление с причиной. Добавьте в проект скрипт проверки JWT_SECRET с fail fast и подключите его к запуску тестов. Настройте один алерт — на всплеск 401 за пять минут — и проверьте его синтетическим перебором на тестовом стенде. В конце соберите короткий документ «почему у нас такие значения»: по строке на каждое отклонение от дефолта, чтобы следующее ревью не начиналось с нуля.
Личные заметкиПусто
Вопросы и ответы по уроку
Пятнадцать минут на access — это точно, или можно иначе?
Это дефолт с границами, а не закон: окно балансирует между частотой refresh и ценой утечки. Короткий exp дисциплинирует ротацию и упрощает отзыв, длинный снижает нагрузку на /refresh. При любых значениях ротация и denylist остаются обязательными — тогда точная цифра перестаёт быть критичной.
Нужен ли RS256 в маленьком проекте?
Не обязательно: при одном сервисе HS256 проще и достаточно при честном секрете. RS256 нужен, когда проверять токены должны сервисы, которым нельзя доверять право выпуска, — из урока про микросервисы. Главное — белый список алгоритмов и свежая библиотека, а семейство выбирается по архитектуре.
Как часто ротировать секрет?
Плановая плановая ротация обычно годовая или полугодовая, внеплановая — немедленно по подозрению в утечке. Благодаря kid частота почти не влияет на пользователей: это операция карты ключей, а не выгон всех сессий. Важнее настроить процесс, чтобы ротация была скучной рутиной, а не аварией.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Telegram-боты на Go: от эха до полноценного сервиса
Учимся писать Telegram-ботов на Go: токен BotFather, команды, клавиатуры, состояния диалога, медиа, рассылки, webhook и деплой. 25 уроков с кодом.
- Создавать бота через BotFather и хранить токен безопасно
- Получать обновления через long polling и webhook
- Отвечать сообщениями, командами и клавиатурами
- Строить многошаговые диалоги с сохранением состояния