Перейти к содержанию

Локальная сессия и защита формы

Приложение уже умеет менять остатки, поэтому пора выделить сведения о текущем посетителе. Добавим серверную сессию и защитный токен формы. В учебном стенде человек явно выбирает вымышленную роль читателя или редактора без проверки личности. Это демонстрация жизненного цикла сессии, а не настоящий вход сотрудника. Сервер остаётся на loopback; перенос такой формы выбора роли на публичный адрес недопустим как готовая авторизация.

Роль, CSRF-токен и время окончания хранятся в памяти сервера. Cookie содержит только случайный идентификатор записи. Значит, человек не получает право редактора простой заменой строки cookie на editor: такое значение не найдёт соответствующей сессии. Однако демонстрационный маршрут выбора роли по-прежнему позволяет получить роль любому локальному посетителю. Эти два свойства нужно различать, чтобы не приписывать случайному идентификатору отсутствующую проверку личности.

Случайный идентификатор и серверное состояние

Идентификатор создаётся из 32 байт через crypto/rand, затем кодируется шестнадцатеричной строкой. Источник случайности нужен для непредсказуемого значения, а не для красивого оформления. В карте sessions по этому ключу лежит Session. Поскольку HTTP-обработчики могут работать одновременно, доступ к карте защищён mutex. Даже локальная страница иногда вызывает несколько параллельных запросов; отсутствие публичных пользователей не отменяет правила одновременного доступа Go.

Если cookie отсутствует, запись не найдена или срок закончился, создаётся анонимная сессия. Она получает свой CSRF-токен и время окончания через час. В cookie указаны HttpOnly, SameSite Strict, Path и MaxAge. HttpOnly запрещает обычному клиентскому JavaScript читать значение cookie, но не предотвращает любое действие вредоносного кода внутри страницы. SameSite ограничивает некоторые внешние сценарии отправки. Это полезные свойства, однако каждое действует на своей границе.

func (a *App) guard(w http.ResponseWriter,r *http.Request)bool{
 if err:=r.ParseForm();err!=nil{http.Error(w,"Неверная форма",400);return false}
 s:=a.session(w,r)
 if subtle.ConstantTimeCompare([]byte(s.CSRF),[]byte(r.PostForm.Get("csrf")))!=1{http.Error(w,"Обновите форму: защитный токен не совпал",403);return false}
 
 return true
}

Фрагмент взят из main.go состояния lesson-12. Полный обработчик включает получение сессии, проверку токена, смену роли и выдачу нового идентификатора. Для локального HTTP cookie не имеет Secure. Настоящий публичный вход должен работать через HTTPS с соответствующими атрибутами, постоянным хранилищем сессий и проверенной идентификацией. Изменение одного флага не превращает демонстрационную схему в завершённую систему доступа.

Токен формы

Каждое HTML-представление получает Page.CSRF из серверной сессии. Шаблон помещает его в скрытое поле всех изменяющих форм, включая выбор роли. При POST сервер разбирает тело и сравнивает полученное значение с текущей сессией через ConstantTimeCompare. Неправильное значение возвращает 403 до SQL. Это защищает связь операции с формой, выданной конкретной сессии, а не подтверждает допустимость названия или количества оборудования.

CSRF обычно объясняют через чужую страницу, которая пытается вызвать действие с автоматически отправленными cookie. В нашем сервере дополнительно используется http.NewCrossOriginProtection: проверка происхождения запроса действует независимо от предметной формы. Защитный токен дополняет эту границу и позволяет увидеть состояние старых форм после смены сессии. Не следует считать, что токен шифрует тело, проверяет личность или заменяет последующую проверку права на изменение.

Токен связан с сессией, а не заново создаётся на каждом GET. Если обновлять его при каждом отображении таблицы, две открытые формы быстро начинают мешать друг другу без содержательной причины. При смене роли выдаём новый идентификатор и новый токен. Старый cookie удаляется из карты. Такая ротация уменьшает возможность продолжения прежнего сессионного основания после изменения состояния входа и делает переход явным.

Завершение входа и старые формы

Выбор anonymous является учебным завершением входа. Сервер заменяет сессию новой анонимной записью, а браузер получает перенаправление на список. В старой вкладке может остаться форма с прежним CSRF. Её последующая отправка ожидаемо откажет, даже если поля оборудования правильные и версия карточки свежая. Причина находится на сессионной границе, а не в конфликте Item.Version. Сообщение предлагает обновить форму, чтобы получить новое основание.

При остановке процесса карта sessions исчезает. Cookie браузера ещё может существовать, но сервер не найдёт запись и сформирует анонимную сессию. Это нормальное свойство выбранного хранилища памяти, а не случайное повреждение cookie. Для нескольких процессов понадобится общий договор сессий; иначе посетитель будет получать разные результаты при попадании на разные экземпляры. Наш учебный сервер остаётся одним процессом и не обещает такой инфраструктуры.

У истёкших записей проверяется время при обращении. Но карта пока не имеет фоновой полной очистки и жёсткого предела числа записей. Локальный стенд ограничен своим сценарием, а публичному сервису потребуются квоты и регулярное удаление истёкших ключей. Таймаут в структуре не является автоматическим освобождением памяти. Эта деталь помогает увидеть разницу между логическим сроком действия и фактическим жизненным циклом ресурсов.

Наблюдаемый договор текущего шага

После самостоятельного открытия страницы анонимный посетитель должен получить cookie и токен формы. Выбор editor создаёт новую сессию и показывает выбранную роль. На данном шаге ещё не ограничиваем сами действия ролью; это задача следующего урока. Иначе курс смешал бы жизненный цикл сессии и предметную политику доступа, а читателю было бы трудно понять, какой именно отказ вызван отсутствием права.

Полезный вариант — открыть форму создания, сменить роль в другой вкладке и отправить прежнюю форму. Ожидается отказ CSRF без записи. Другой вариант — изменить поле role на произвольную строку. Маршрут выбора должен отклонить неизвестное значение, не создавая сессию с непредусмотренной ролью. Такие сценарии приведены как инструкции самостоятельного прохождения: код, браузерная сессия и проверки при подготовке не запускались.

Следующий шаг использует уже полученную Session как серверный источник решения. Viewer сможет читать список, editor — создавать карточки, редактировать и проводить движения. Скрытие кнопки поможет интерфейсу, но не станет границей права. Каждый POST должен самостоятельно решить, допустимо ли изменение для текущего субъекта, независимо от формы, заголовка htmx и того, каким клиентом отправлено тело.

Cookie имеет Path для всего приложения, поэтому одна сессия используется и списком, и маршрутом движения. Если задать путь только для страницы входа, последующие формы не получат тот же идентификатор. Такой отказ внешне похож на неправильный токен, но причина находится в области отправки cookie, а не в случайном генераторе.

Исходники урока

Оглавление серии · Скачать исходники. Полное самостоятельное состояние находится в lesson-12. Учебные приложения, зависимости, база и проверки при подготовке не запускались; описанный результат ожидается по исходникам. Материалы пока находятся в локальной редакционной версии.

Первичные источники: документация 1.