Витрина Melbis работает почти в режиме «только чтение» и агрессивно кешируется, поэтому всё, что относится к конкретному посетителю — корзина, авторизация, выбранный язык, — живёт в сессии и куках. Это единственный слой данных, который у каждого посетителя свой.
Этот раздел — про серверную часть: сессию посетителя и защиту форм её токеном. Про куки рассказано в соседнем разделе «Куки», про то, что известно о пришедшем из заголовков запроса, — в разделе «Посетитель».
Сессия создаётся один раз, в корневом скрипте, до вызова
Run():
// index.php
require 'units/melbis.php';
MELBIS()->DefineSession('MELBIS_SHOP');
MELBIS()->Run($entry_point, $entry_param);Полная сигнатура:
DefineSession($mName, $mDomain = '', $mLifeTime = 0, $mPath = '/', $mSecure = true)$mName — имя cookie сессии.$mDomain — домен; пусто означает текущий.$mLifeTime — время жизни в секундах; 0 —
до закрытия браузера.$mPath — путь, для которого действует cookie.$mSecure — передавать cookie только по HTTPS.Помимо собственно старта сессии метод делает две вещи: генерирует CSRF-токен, если его ещё нет, и включает режим отладки, если в URL пришёл секретный код (см. «Отладчик»).
Если сессия не создана, методы работы с ней вызывать нельзя —
обращение к ним без DefineSession приведёт к ошибке.
// Записать
MELBIS()->SessionSetValue('client_id', $client_id);
// Прочитать
$client_id = MELBIS()->SessionGetValue('client_id');
// Удалить одно значение
MELBIS()->SessionRemoveValue('client_id');
// Уничтожить сессию целиком
MELBIS()->SessionRemove();SessionGetValue возвращает null, если
значения нет, — это удобно отличать от пустой строки, но требует
аккуратности при проверках:
$value = MELBIS()->SessionGetValue('basket_id');
$exists = !is_null($value);SessionRemove очищает данные, уничтожает сессию и гасит
cookie — это выход из учётной записи целиком, а не удаление одного
ключа.
Идентификатор текущей сессии отдаёт SessionGetId(). Он
же доступен в любом шаблоне системным тегом
{PHP_SESS_ID}.
PHP держит файл сессии заблокированным всё время работы скрипта. Пока запрос не завершится, параллельные запросы того же посетителя — AJAX-вызовы, отложенная загрузка блоков — будут ждать в очереди, даже если сессия им нужна только на чтение.
SessionSuspend() закрывает сессию на запись и снимает
блокировку, SessionResume() открывает её снова:
// Данные из сессии уже прочитаны, дальше идёт долгая работа
MELBIS()->SessionSuspend();
$answer = MELBIS_INC_CURL_post($url, $data); // внешний API, несколько секунд
// Сессия нужна снова — записать результат
MELBIS()->SessionResume();
MELBIS()->SessionSetValue('last_answer', $answer);Это единственный способ не заставлять параллельные запросы посетителя
ждать медленный модуль. Правило простое: между Suspend и
Resume сессия недоступна — ни на чтение, ни на запись, —
поэтому всё нужное из неё надо прочитать заранее.
CSRF-атака — это запрос, отправленный на сайт со стороннего ресурса
от имени залогиненного посетителя: чужая страница шлёт POST
на ваш обработчик, а браузер послушно прикладывает к нему куки сессии.
Защита от этого одна — проверять, что форма действительно была отдана
вашей витриной. Для этого служит токен, привязанный к сессии
посетителя.
Токен создаётся автоматически при DefineSession. В
шаблон он попадает системным тегом {CSRF_TOKEN} —
передавать его из модуля не нужно, тег доступен в любом шаблоне:
<form method="post" action="/order/">
<input type="hidden" name="csrf" value="{CSRF_TOKEN}">
...
</form>Модуль-обработчик сверяет пришедшее значение:
$token = $mVars['post']['csrf'] ?? '';
if ( !MELBIS()->SessionCsrfCheck($token) )
{
// Запрос пришёл не с нашей формы
return MELBIS()->TplFinal($tpl, 'error');
}SessionCsrfCheck возвращает true только при
точном совпадении с токеном из сессии. Сравнение идёт устойчивым к атаке
по времени способом, поэтому подбирать токен посимвольно бесполезно.
Если сессия не создана, метод возвращает false — проверка
не проходит, а не падает с ошибкой.
Когда форму отправляет не браузер, а скрипт — AJAX, JSON, отложенная загрузка, — тега в разметке может не быть вовсе. Тогда токен берётся из PHP и кладётся в ответ модуля сам:
$token = MELBIS()->SessionCsrfToken();Проверяется он потом тем же SessionCsrfCheck —
принимающей стороне всё равно, из какой формы пришло значение.
Проверку имеет смысл ставить в каждом модуле, который что-то меняет
по POST: оформление заказа, отправка формы обратной связи,
изменение корзины. На обычных страницах, которые только читают данные,
она не нужна.
Результат модуля кешируется вместе с его локальными данными. Поэтому значения из сессии нельзя выводить как обычную переменную модуля — первый же посетитель запечёт своё имя в кеш, и его увидят остальные.
Персональные данные передаются только через глобальный массив: он разворачивается на отдельном проходе, который не кешируется никогда. Так шапку сайта можно кешировать целиком, а приветствие всё равно будет персональным.
Готовый пример — в разделе «Глобальный массив» → «Данные посетителя: сессия и кеш».