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

Сессия и состояние клиента

В предыдущем уроке браузер получил cookie с непрозрачным id. Само значение не содержит полного профиля редактора и не делает запрос разрешённым автоматически. Теперь определим серверную сессию: запись, которая связывает предъявленный идентификатор с человеком, временем доступа и дополнительными правилами.

Результат урока — понятный жизненный цикл сессии Alice. Мы выберем конкретные сроки и поведение после выхода, но не создадим работающий обработчик входа. Все записи и значения являются учебной моделью. Сравнение состояния помогает проектировать будущую реализацию, а не подтверждает её безопасность без исполнения.

Идентификатор ссылается на запись

В cookie находится условное sid-alice. Сервер не извлекает из имени права. Он ищет соответствующую запись по защищённому представлению идентификатора и проверяет её действительность. Полную концептуальную запись можно представить таким JSON:

{
  "subject":"u17",
  "role":"editor",
  "topics":["frontend"],
  "createdAt":"2026-10-11T10:00:00Z",
  "lastSeenAt":"2026-10-11T10:10:00Z",
  "absoluteExpiresAt":"2026-10-11T18:00:00Z",
  "csrfToken":"csrf-alice",
  "revoked":false
}

Этот объект не отправляется в cookie и не публикуется целиком через /api/me. Например, CSRF-токен и служебные отметки имеют другие задачи. В настоящем хранилище также понадобится защищённый ключ поиска, а условные строки заменятся криптографически случайными значениями. Здесь JSON показывает поля состояния, а не готовую схему базы.

Пользователь u17 — Alice. Роль editor даёт общий вид возможностей, а topics ограничивает область frontend. Сервер проверяет актуальные права при каждой операции; сессия не обязана бессрочно сохранять права, существовавшие при входе. Если редактору отозвали доступ, новый запрос должен учитывать изменение по выбранной политике проверки.

Два разных срока

Выбираем idle timeout 30 минут и абсолютный срок восемь часов от создания. Idle означает отсутствие принятой активности в течение заданного периода. Абсолютный срок ограничивает жизнь сессии даже при постоянных обращениях. Это прикладные значения нашего учебного договора, а не универсальная рекомендация для всех сервисов.

Если последняя активность была в 10:10, обращение после 10:40 выходит за idle-предел по принятому сравнению времени. Даже при нескольких успешных обращениях сессия не действует после 18:00. Сервер использует свои часы и последовательное правило границ; клиентское изменение календаря не продлевает доступ.

Временная cookie браузера может сохраняться после этих моментов. Сервер всё равно возвращает отказ действительности. Напротив, удаление cookie человеком не обязано сразу стирать серверную запись, поскольку сервер ещё не получил logout. Поэтому нельзя свести жизненный цикл к закрытию окна или наличию строки в хранилище браузера.

Продление lastSeenAt должно иметь понятную область. Например, публичный GET курса не должен автоматически подтверждать редакторскую активность, если сервис для него вообще не использует сессию. В нашей модели срок поддерживают принятые защищённые обращения. Точная частота записи и распределённые часы относятся к реализации, но нельзя оставлять семантику скрытой.

Ротация после доверенного события

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

Ротация уменьшает риск фиксации сессии, когда атакующая сторона заранее знает используемый id. Она не решает любую кражу действующей cookie и не заменяет защищённый транспорт. Если похищенное значение ещё действительно, обладатель способен предъявить его. Серверный срок, отзыв и защита хранения остаются необходимыми частями модели. Жизненный цикл сессии OWASP.

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

Выход прекращает конкретную сессию

Для выхода вводим POST /api/session/logout. Это изменение состояния, поэтому GET-ссылка не должна скрыто выполнять отзыв. Cookie-аутентифицированный logout требует тот же CSRF-договор, что и другие изменения; подробный механизм введём в уроке 23.

POST /api/session/logout HTTP/1.1
Host: catalog.example
Cookie: __Host-catalog_sid=sid-alice
Origin: https://catalog.example
X-CSRF-Token: csrf-alice

После принятия запись получает revoked, а ответ 204 удаляет cookie. Новое обращение с прежним значением не восстанавливает её. Так серверный отзыв и просьба браузеру о удалении поддерживают один ожидаемый результат, хотя работают в разных хранилищах.

Если соединение оборвалось после отзыва, клиент мог не увидеть удаление cookie. Повторный защищённый запрос с ней должен отказать. Это не означает, что logout обязательно не сработал. Как и при редактировании, отсутствие ответа оставляет неопределённость, которую нужно объяснить, не выдавая действующий экран за подтверждение текущего доступа.

Сессия и состояние интерфейса

Фильтр каталога и выбранная карточка относятся к состоянию клиента. Их не обязательно хранить в сессии. Публичный запрос с topic frontend уже объясняет выборку. Если сервер вынужден вспоминать «последний фильтр» из другой вкладки, адрес перестаёт сам определять результат, а независимые окна начинают мешать друг другу.

Сессия нужна прежде всего для связанного доступа. Интерфейс может хранить свой фильтр в URL, локальную форму в памяти и сведения о незавершённой операции отдельно. Очистка локального фильтра не завершает сессию, а logout не обязан удалять все пользовательские файлы. Эти действия имеют разные предметные результаты.

CSRF-токен связан с конкретной сессией, но доступен доверенному клиенту отдельным защищённым GET /api/csrf. Ответ не кешируется. Токен не заменяет id сессии и не является доказательством роли: операция проверяет оба основания плюс право на конкретный ресурс. Отправка одного csrf-alice без действующего sid-alice не создаёт доступ.

Обновление активности не должно оживлять отзыв

Представим два одновременных обращения: одно фиксирует lastSeenAt, другое завершает сессию. Если первое поздно сохранит старую копию всей записи с revoked:false, оно способно случайно восстановить доступ. Изменение активности должно сохранять уже принятое прекращение, а не переписывать независимые поля устаревшим объектом.

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

Несколько устройств и аудит

Две сессии одного пользователя имеют разные идентификаторы и жизненные циклы. Выход из одной не обещает завершение другой. Если продукт вводит «выйти везде», ему потребуется отдельная операция, способная отозвать все соответствующие записи. Не стоит утверждать такую функцию на основании удаления единственной cookie.

В журнале полезно связывать действия с subject и безопасным идентификатором события, не записывая сырой session id. Секрет в журнале становится дополнительным местом риска. Диагностика должна помогать узнать, какая сессия была отозвана, не превращая лог в набор пригодных credentials.

Теперь проследите один сценарий: вход, чтение профиля, пауза, истечение idle и повторный запрос. Отдельно рассмотрите активную работу до абсолютного срока и выход после обрыва. В каждом случае серверная запись даёт конкретное объяснение. Следующий урок переведёт эту модель в договор аутентификации API и покажет различие между неизвестной личностью и известным пользователем без прав.

Примеры продолжения серии. Оглавление курса.