Глава 30 · Практика
Сессии и вход: cookies, блокировки и смена идентификатора
Как сервер узнаёт покупателя и почему медленный AJAX способен остановить соседнюю вкладку.
Зачем сессия: узнаём пользователя в следующем запросе
Нужен простой результат: на первой странице выбрать русский язык, на второй увидеть тот же выбор. Локальные переменные первого запроса исчезнут; сессия сохранит значение на сервере. Браузер получает только непрозрачный ключ cookie, а не весь массив данных.
- GET /save.php → $_SESSION[language] = Русский
- Ответ Set-Cookie: PHPSESSID=случайный ключ
- GET /read.php с Cookie → PHP читает language
- Страница: Ваш язык: Русский.
session_start() вызывают до тела HTML: скобки запускают функцию. $_SESSION — массив текущей сессии; ['language'] выбирает ключ, = сохраняет значение. session_write_close() записывает изменения и закрывает хранилище. Cookie браузер отправит автоматически следующему запросу в подходящей области.
Распакуйте архив. Выполните php -S 127.0.0.1:8793 из его папки, откройте http://127.0.0.1:8793/save.php, затем ссылку «Следующая страница». Это намеренно заданный несекретный выбор для изучения механизма; пользовательские изменения и вход далее отправляются POST-формой.
<?php
declare(strict_types=1);
session_start(['use_strict_mode' => true]);
$_SESSION['language'] = 'Русский';
session_write_close();
header('Content-Type: text/html; charset=UTF-8');
echo '<!doctype html><html lang="ru"><meta name="viewport" content="width=device-width,initial-scale=1">';
echo '<title>Выбор языка</title><p>Язык сохранён: Русский.</p><a href="/read.php">Следующая страница</a></html>';
Ожидаемый результат
Первая страница: Язык сохранён: Русский.
По ссылке: Ваш язык: Русский.
В новом приватном окне: Ваш язык: ещё не выбран.
Запуск проверен реальными HTTP-запросами с cookie и без неё. Эта первая ступень не проверяет пароль и не объявляет наличие сессии входом.
Во втором файле ?? выбирает запасной текст, если language отсутствует. htmlspecialchars превращает значение в безопасный HTML-текст. __DIR__ далее означает каталог текущего PHP-файла; require подключает общий файл до продолжения страницы.
Полный файл read.php
<?php
declare(strict_types=1);
session_start(['use_strict_mode' => true]);
$language = $_SESSION['language'] ?? 'ещё не выбран';
session_write_close();
header('Content-Type: text/html; charset=UTF-8');
echo '<!doctype html><html lang="ru"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Язык</title>';
echo '<p>Ваш язык: ', htmlspecialchars($language, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'), '.</p></html>';
Авторизация отвечает на другой вопрос: разрешено ли этому пользователю конкретное действие. user_id записывает только проверенный серверный обработчик. Корзина не хранит окончательные цены/права: они могли измениться и читаются из актуального источника.
CSRF до большого проекта: зачем hidden-поле и что именно сравнивается
Чужая страница способна отправить форму на ваш домен: браузер приложит cookie входа автоматически. Синхронизирующий токен добавляет значение, которого у чужой страницы нет. Сервер создаёт его случайно, хранит в своей сессии, помещает в hidden-поле собственной формы и проверяет в каждом запросе, меняющем данные.
- GET формы → сервер создаёт случайный csrf и сохраняет в $_SESSION
- HTML: hidden name="csrf" получает это значение
- Браузер отправляет POST: обычные поля + csrf + cookie сессии
- Сервер принимает mixed → требует строку → hash_equals(эталон, полученное)
- Совпало → выполняет действие; нет → HTTP 403 без изменения данных
В коде ниже случайное значение намеренно не печатается. Мы наблюдаем решение, а не секрет. mixed нужен на границе запроса: злоумышленник может прислать csrf[]=x, поэтому сначала вызывается is_string. Только после этого hash_equals получает две строки; известный серверный эталон передаётся первым аргументом.
<?php
declare(strict_types=1);
$session = ['csrf' => bin2hex(random_bytes(32))];
function csrfAccepted(mixed $sent, string $known): bool
{
return is_string($sent) && hash_equals($known, $sent);
}
$attempts = [
'Токен из нашей формы' => $session['csrf'],
'Другая строка' => 'wrong-token',
'Массив вместо строки' => ['wrong-token'],
];
foreach ($attempts as $label => $sent) {
echo $label, ': ', csrfAccepted($sent, $session['csrf'])
? "действие разрешено\n"
: "действие отклонено\n";
}
Ожидаемый результат
Токен из нашей формы: действие разрешено
Другая строка: действие отклонено
Массив вместо строки: действие отклонено
Это минимальная механика без HTTP. Следующий проект проверяет ту же схему реальными GET/POST, cookie и кодами 403/200. SameSite помогает браузеру ограничить межсайтовую отправку cookie, но в общем случае остаётся дополнительным слоем, а не заменой токена.
| Поле формы | Что читает PHP | Кому доверять |
|---|---|---|
name="username" | $_POST['username'] | Это внешний ввод: проверить тип/длину; имя не выдаёт полномочий. |
name="password" | $_POST['password'] | Не trim, не возвращать в HTML и журнал; передать в password_verify без изменения. |
name="csrf" | $_POST['csrf'] | Сравнить с серверной $_SESSION['csrf']; hidden не является доверенным сам по себе. |
| Cookie сессии | Её разбирает модуль session | ID связывает запрос с серверным состоянием, но не доказывает право на каждый объект. |
Полная связь с HTML-формой разобрана в главе о формах. Настройки cookie и блокировки — в полном справочнике сессий.
Рабочий кабинет: от формы входа до выхода
Получится маленький кабинет Анны: форма принимает имя и пароль, неверный ввод оставляет понятную ошибку, успешный показывает заказ №42. Кнопка выхода убирает текущий вход. Архив содержит все файлы; ничего из прежних глав копировать не требуется.
| Шаг | Что сделать и увидеть |
|---|---|
| Подготовка | Распаковать архив → php setup.php. Созданы private/users.json и private/sessions/. Учебный аккаунт anna, пароль Локальный кабинет 2026!. |
| Сервер | php -S 127.0.0.1:8793 -t public из корня архива. Открыть http://127.0.0.1:8793/login.php. Папка private не является web-root. |
| Ошибка | Ввести anna и wrong. Ответ401, текст «Неверное имя или пароль». Поле имени остаётся заполненным; пароль никогда не возвращается в HTML. |
| Успех | Ввести учебную фразу. POST получает303 Location:/account.php; GET кабинета показывает «Здравствуйте, Анна!» и «Заказ №42: готов к выдаче». |
| Выход | Кнопка «Выйти» отправляет POST. Страница «Вы вышли»; повторный account.php ведёт к форме входа. GET logout.php не завершает вход. |
- login.php GET: сессия гостя и случайный CSRF → HTML hidden csrf
- POST: username/password/csrf → проверка типов и CSRF
- private/users.json: hash → password_verify
- regenerate_id(false) → только новый ID получает auth.id7
- account.php: сроки и auth → имя Анна и заказ42
- logout.php POST: CSRF → очистка массива, записи и cookie
Сначала разберите файлы по порядку ниже. В форме name="username" становится ключом $_POST['username']; password и csrf работают так же. Поле type=password скрывает символы на экране, но не шифрует сеть. Hidden тоже присылает клиент: сервер сравнивает его со своим токеном. Наличие required помогает интерфейсу; сервер всё равно отвергает массив вместо строки, слишком длинный ввод и неверный токен.
В контроллере if выбирает ветку; !== проверяет точное несовпадение; || означает «или», && — «и». ['auth'] — ключ вложенного массива. ['id' => 7] связывает имя с числом. Вызовы h, page, go, validToken, currentUser определены в bootstrap ниже; это наши функции, не дополнительные расширения PHP.
| Помощник | Вход, действие и результат |
|---|---|
| h(text) | Получает строку → htmlspecialchars → возвращает безопасный HTML-текст. Экранируем имя при выводе, исходный пароль не меняем. |
| page(title, body) | Получает заголовок и уже собранный безопасный HTML → закрывает сессию → печатает документ и exit. Тип never означает, что выполнение к вызывающему коду не вернётся. |
| go(path) | Получает только заданный в коде локальный путь → закрывает сессию →303 Location → exit. Пользовательский URL сюда не попадает. |
| validToken(sent) | mixed разрешает сначала принять любой PHP-тип → is_string → hash_equals(серверный эталон, присланное значение). |
| currentUser(now) | Получает серверное время → читает auth → проверяет last_seen и created → возвращает массив пользователя либо null. Обновляет last_seen только при ещё допустимом входе. |
Полный файл setup.php
<?php
declare(strict_types=1);
if (PHP_SAPI !== 'cli') { exit('Запустите из терминала.'); }
$path = __DIR__ . '/private';
if (!is_dir($path . '/sessions') && !mkdir($path . '/sessions', 0700, true)) { throw new RuntimeException('Не создан каталог сессий'); }
$users = ['anna' => ['id' => 7, 'name' => 'Анна', 'hash' => password_hash('Локальный кабинет 2026!', PASSWORD_BCRYPT)]];
$bytes = json_encode($users, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR);
if (file_put_contents($path . '/users.json', $bytes, LOCK_EX) !== strlen($bytes)) { throw new RuntimeException('Учётная запись не сохранена'); }
echo "Создан учебный аккаунт anna. Пароль хранится в виде хеша.\n";
setup.php выполняется в терминале. json_encode формирует запись аккаунта, file_put_contents сохраняет её; сравнение числа записанных байтов обнаруживает неполную запись. Лаборатория явно выбирает PASSWORD_BCRYPT, поэтому обработчик до хеширования отклоняет пароль длиннее 72 байтов и не обрезает его молча. В PHP 8.4 стандартный cost bcrypt вырос с 10 до 12; рабочее приложение измеряет свою задержку. Соль не выбирается вручную. В проекте один фиксированный учебный аккаунт; последовательная регистрация, смена и восстановление разобраны ниже в password-reference.
Полный файл private/bootstrap.php
<?php
declare(strict_types=1);
// This project is deliberately limited to the local PHP teaching server.
if (PHP_SAPI !== 'cli-server' || ($_SERVER['REMOTE_ADDR'] ?? '') !== '127.0.0.1') {
http_response_code(503); exit('Этот учебный проект работает только на 127.0.0.1.');
}
$private = __DIR__;
if (!is_dir($private . '/sessions') || !is_file($private . '/users.json')) {
http_response_code(503); exit('Сначала выполните php setup.php в папке проекта.');
}
ini_set('session.use_strict_mode', '1');
ini_set('session.use_only_cookies', '1');
ini_set('session.use_trans_sid', '0');
session_name('ManualLogin');
session_save_path($private . '/sessions');
session_set_cookie_params([
'lifetime' => 0, 'path' => '/', 'domain' => '',
'secure' => false, // Only this loopback HTTP lab; HTTPS deployment requires true.
'httponly' => true, 'samesite' => 'Lax',
]);
if (!session_start()) { http_response_code(503); exit('Вход временно недоступен.'); }
header('Content-Type: text/html; charset=UTF-8');
header('Cache-Control: no-store');
header("Content-Security-Policy: default-src 'none'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'");
header('X-Content-Type-Options: nosniff');
// Only the local CLI helper can set the teaching clock; no request field does so.
$clockFile = $private . '/clock.txt';
$now = is_file($clockFile) ? (int) file_get_contents($clockFile) : time();
if (!isset($_SESSION['csrf'])) { $_SESSION['csrf'] = bin2hex(random_bytes(32)); }
function h(string $text): string { return htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
function page(string $title, string $body): never {
session_write_close();
echo '<!doctype html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>', h($title), '</title></head><body><main><h1>', h($title), '</h1>', $body, '</main></body></html>';
exit;
}
function go(string $path): never {
session_write_close(); header('Location: ' . $path, true, 303); exit;
}
function validToken(mixed $sent): bool {
return is_string($sent) && hash_equals($_SESSION['csrf'], $sent);
}
function currentUser(int $now): ?array {
$auth = $_SESSION['auth'] ?? null;
if (!is_array($auth)) { return null; }
if ($now - $auth['last_seen'] >= 30 || $now - $auth['created'] >= 120) {
unset($_SESSION['auth']); $_SESSION['expired'] = true; return null;
}
$_SESSION['auth']['last_seen'] = $now;
return $auth;
}
bootstrap настраивает session до любого echo. Имя cookie ManualLogin одинаково для всех посетителей. Данные лежат вне public. На локальном HTTP Secure=false; для настоящего HTTPS нужно Secure=true, сам стенд намеренно отказывается обслуживать другие SAPI/адреса. HttpOnly запрещает чтение cookie JavaScript, SameSite=Lax ограничивает ряд межсайтовых передач; оба не заменяют CSRF и безопасный HTML-вывод.
| Реально проверенный HTTP-шаг | Значимая часть запроса/ответа |
|---|---|
| GET /login.php →200 | Set-Cookie: ManualLogin=[гостевой ID]; path=/; HttpOnly; SameSite=Lax. Случайная часть скрыта в отчёте. |
| POST /login.php | Cookie: ManualLogin=[гостевой ID]; тело form-urlencoded содержит username=anna, UTF-8-байты password и csrf из формы. |
| Успешный POST →303 | Location: /account.php и Set-Cookie с новым ID. Проверка сравнила старый и новый ключи, не выводя их. |
| GET /account.php →200 | Новая cookie восстанавливает auth.id7; ответ содержит Cache-Control: no-store и HTML приветствия. |
| POST /logout.php →200 | Set-Cookie: ManualLogin=[удалено]; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; HttpOnly; SameSite=Lax. |
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/private/bootstrap.php';
if (currentUser($now) !== null) { go('/account.php'); }
$message = !empty($_SESSION['expired']) ? 'Срок входа истёк. Войдите снова.' : '';
unset($_SESSION['expired']);
$username = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!validToken($_POST['csrf'] ?? null)) {
http_response_code(403); page('Форма устарела', '<p>Откройте форму входа заново.</p><a href="/login.php">Войти</a>');
}
$name = $_POST['username'] ?? null;
$password = $_POST['password'] ?? null;
// This fixture deliberately uses bcrypt, so its documented 72-byte limit applies.
if (!is_string($name) || !is_string($password) || strlen($name) > 40 || strlen($password) > 72 || str_contains($password, "\0")) {
http_response_code(400); $message = 'Поля входа должны быть строками допустимой длины.';
} else {
$username = trim($name); // The password is deliberately not trimmed or escaped.
$users = json_decode(file_get_contents(dirname(__DIR__) . '/private/users.json'), true, 512, JSON_THROW_ON_ERROR);
$user = $users[$username] ?? null;
// A stored dummy hash has the same cost; no hash is generated on a failed attempt.
$hash = $user['hash'] ?? $users['anna']['hash'];
$matches = password_verify($password, $hash);
if (!$matches || $user === null) {
http_response_code(401); $message = 'Неверное имя или пароль.';
} else {
// The old guest record receives no authentication data.
$_SESSION['retired_at'] = $now;
if (!session_regenerate_id(false)) { http_response_code(503); page('Вход недоступен', '<p>Повторите попытку позже.</p>'); }
unset($_SESSION['retired_at']);
$_SESSION['csrf'] = bin2hex(random_bytes(32));
$_SESSION['auth'] = ['id' => $user['id'], 'name' => $user['name'], 'created' => $now, 'last_seen' => $now];
go('/account.php');
}
}
}
$body = '<p>Учебный аккаунт: anna. Пароль: Локальный кабинет 2026!</p>';
if ($message !== '') { $body .= '<p role="alert">' . h($message) . '</p>'; }
$body .= '<form method="post" action="/login.php"><input type="hidden" name="csrf" value="' . h($_SESSION['csrf']) . '">'
. '<p><label>Имя <input name="username" autocomplete="username" maxlength="40" required value="' . h($username) . '"></label></p>'
. '<p><label>Пароль <input type="password" name="password" autocomplete="current-password" required></label></p>'
. '<button type="submit">Войти</button></form>';
page('Вход в учебный кабинет', $body);
Ожидаемый результат
Форма: Вход в учебный кабинет
Неверный пароль: Неверное имя или пароль.
Правильный пароль: Здравствуйте, Анна!
Заказ №42: готов к выдаче.
Старый гостевой ID: личный кабинет недоступен.
Выход: Вы вышли
Повторный доступ: форма входа.
30 секунд бездействия: Срок входа истёк. Войдите снова.
120 секунд общего срока: Срок входа истёк. Войдите снова.
Поддельный CSRF: Выход не выполнен
Массив вместо пароля: Поля входа должны быть строками допустимой длины.
Cookie: ManualLogin; Path=/; HttpOnly; SameSite=Lax; ID сменён при входе.
В архиве example.php — копия показанного контроллера для чтения; запускайте проект командой с -t public из README, где рабочая копия называется public/login.php. Протокол проверен фактически на localhost. Случайные ID и токены не печатаются как готовые значения: HTTP-проверка сравнивает их внутри процесса.
Проверка пароля выполняется даже для неизвестного имени по заранее сохранённому dummy-хешу той же политики; это не обещание одинакового времени всего HTTP-запроса. После успеха регенерация идёт до назначения auth. Старый ID был гостем и не получает права; потеря новой cookie требует повторить вход. Повторная ротация уже авторизованных параллельных запросов требует другой политики timestamps/переходного периода, разобранной в полном session-reference.
Полный файл public/account.php
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/private/bootstrap.php';
$user = currentUser($now);
if ($user === null) { go('/login.php'); }
page('Личный кабинет', '<p>Здравствуйте, ' . h($user['name']) . '!</p>'
. '<p>Заказ №42: готов к выдаче.</p>'
. '<form method="post" action="/logout.php"><input type="hidden" name="csrf" value="' . h($_SESSION['csrf']) . '"><button type="submit">Выйти</button></form>');
account.php не доверяет факту перехода с login.php. Каждый запрос заново вызывает currentUser. Имя берётся из закрытой серверной записи. Для приложения с разными заказами дополнительно проверяется владелец каждой записи по текущему ID; один корректный номер заказа не является разрешением его читать.
Полный файл public/logout.php
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/private/bootstrap.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Allow: POST'); http_response_code(405); page('Нужна кнопка выхода', '<a href="/account.php">Личный кабинет</a>');
}
if (!validToken($_POST['csrf'] ?? null)) {
http_response_code(403); page('Выход не выполнен', '<p>Откройте личный кабинет и нажмите кнопку выхода.</p>');
}
$policy = session_get_cookie_params();
$_SESSION = [];
if (!session_destroy()) { http_response_code(503); exit('Не удалось завершить сессию.'); }
setcookie(session_name(), '', [
'expires' => 1, 'path' => $policy['path'], 'domain' => $policy['domain'],
'secure' => $policy['secure'], 'httponly' => $policy['httponly'], 'samesite' => $policy['samesite'],
]);
echo '<!doctype html><html lang="ru"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Выход</title><h1>Вы вышли</h1><p>Личный кабинет теперь требует нового входа.</p><a href="/login.php">Войти снова</a></html>';
В выходе три разных действия: очистить $_SESSION в текущем запросе, удалить запись через session_destroy, истечь cookie с тем же path/domain. Ответ с удалением cookie не удаляет копии, ранее похищенные с других устройств, и не отзывает всю учётную запись. Для выхода везде нужна серверная модель сессий/версии и проверка отзыва на каждом защищённом запросе.
Истечение, поддельные поля и наблюдаемые отказы
У проекта два срока: 30секунд бездействия и 120секунд с момента входа. Это короткие учебные значения. Удаление старых файлов сборщиком мусора не определяет, разрешён ли доступ прямо сейчас.
| Проверка | Действия и точный результат |
|---|---|
| Простой | В другом терминале php clock.php 1000, войти. Затем php clock.php 1030, обновить кабинет: «Срок входа истёк. Войдите снова». |
| Общий срок | Войти при1000. Открыть кабинет при1020/1040/1060/1080/1100 — он доступен. При1120 новый запрос возвращает форму с тем же сообщением, хотя последний был20секунд назад. |
| Без CSRF | POST logout без hidden-токена:403 «Выход не выполнен»; защищённая страница по-прежнему доступна. |
| Поддельный тип | Отправить password[]=x с настоящим csrf:400 «Поля входа должны быть строками допустимой длины». |
| Хранилище не подготовлено | Запустить без setup:503 «Сначала выполните php setup.php в папке проекта». |
Полный файл clock.php
<?php
declare(strict_types=1);
if (PHP_SAPI !== 'cli') { exit('Только терминал.'); }
$value = $argv[1] ?? '';
if (!ctype_digit($value)) { exit("Укажите учебные секунды: php clock.php 1000\n"); }
if (file_put_contents(__DIR__ . '/private/clock.txt', $value) === false) { throw new RuntimeException('Часы не установлены'); }
echo 'Учебное время: ', $value, "\n";
Часы читает только закрытый серверный файл. Ни поле формы, ни query-параметр не меняют время. Без clock.txt используется time(). Инструмент нужен для проверки сроков без ожиданий; это часть локальной лаборатории, не публичный endpoint.
Отдельный архив ниже намеренно запускается без setup.php. Bootstrap останавливает вход до создания сессии, поэтому отсутствие учётного хранилища не превращается в успешную авторизацию.
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/private/bootstrap.php';
if (currentUser($now) !== null) { go('/account.php'); }
$message = !empty($_SESSION['expired']) ? 'Срок входа истёк. Войдите снова.' : '';
unset($_SESSION['expired']);
$username = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!validToken($_POST['csrf'] ?? null)) {
http_response_code(403); page('Форма устарела', '<p>Откройте форму входа заново.</p><a href="/login.php">Войти</a>');
}
$name = $_POST['username'] ?? null;
$password = $_POST['password'] ?? null;
// This fixture deliberately uses bcrypt, so its documented 72-byte limit applies.
if (!is_string($name) || !is_string($password) || strlen($name) > 40 || strlen($password) > 72 || str_contains($password, "\0")) {
http_response_code(400); $message = 'Поля входа должны быть строками допустимой длины.';
} else {
$username = trim($name); // The password is deliberately not trimmed or escaped.
$users = json_decode(file_get_contents(dirname(__DIR__) . '/private/users.json'), true, 512, JSON_THROW_ON_ERROR);
$user = $users[$username] ?? null;
// A stored dummy hash has the same cost; no hash is generated on a failed attempt.
$hash = $user['hash'] ?? $users['anna']['hash'];
$matches = password_verify($password, $hash);
if (!$matches || $user === null) {
http_response_code(401); $message = 'Неверное имя или пароль.';
} else {
// The old guest record receives no authentication data.
$_SESSION['retired_at'] = $now;
if (!session_regenerate_id(false)) { http_response_code(503); page('Вход недоступен', '<p>Повторите попытку позже.</p>'); }
unset($_SESSION['retired_at']);
$_SESSION['csrf'] = bin2hex(random_bytes(32));
$_SESSION['auth'] = ['id' => $user['id'], 'name' => $user['name'], 'created' => $now, 'last_seen' => $now];
go('/account.php');
}
}
}
$body = '<p>Учебный аккаунт: anna. Пароль: Локальный кабинет 2026!</p>';
if ($message !== '') { $body .= '<p role="alert">' . h($message) . '</p>'; }
$body .= '<form method="post" action="/login.php"><input type="hidden" name="csrf" value="' . h($_SESSION['csrf']) . '">'
. '<p><label>Имя <input name="username" autocomplete="username" maxlength="40" required value="' . h($username) . '"></label></p>'
. '<p><label>Пароль <input type="password" name="password" autocomplete="current-password" required></label></p>'
. '<button type="submit">Войти</button></form>';
page('Вход в учебный кабинет', $body);
Ожидаемый результат
HTTP503: Сначала выполните php setup.php в папке проекта.
Реальный localhost запрос проверен в отдельной папке без users.json и sessions/. Рабочий проект и его данные не меняются. После обычной подготовки первый архив разрешает вход учебному аккаунту.
Cookie содержит идентификатор, сервер хранит состояние
Cookie связывает новый запрос с серверной сессией
После входа сайт узнаёт покупателя при следующем запросе, хотя прежние локальные переменные PHP уже исчезли.
- Браузер отправляет cookie
В cookie обычно находится непрозрачный ID сессии. Сама серверная корзина там не хранится.
- session_start() на сервере
По ID загружается запись сессии. Обработчик хранения может удерживать её блокировку.
- PHP читает состояние
Например, user_id=42 после проверенного входа. Право на конкретный заказ проверяется отдельно.
- Сохранить и освободить
session_write_close() сохраняет изменения и позволяет отпустить блокировку до долгой работы.
При успешном входе меняют ID сессии. Cookie — идентификатор; запись сессии и локальные переменные запроса — разные вещи.
HTTP сам по себе не помнит посетителя. При cookie-сессии браузер передаёт идентификатор, а PHP загружает соответствующие данные из серверного хранилища. Обычно в сессии достаточно user_id, CSRF-токена и краткого flash-сообщения. Цены, права и состояние заказа лучше читать из актуального источника: пользователь мог потерять роль или товар подорожал после входа.
Идентификатор сессии обладает силой временного ключа доступа. HTTPS, Secure, HttpOnly и подходящий SameSite уменьшают способы его утечки. HttpOnly не мешает XSS выполнять действия от имени пользователя, поэтому защита вывода остаётся необходимой. Cookie-параметры задают до session_start. Не передавайте ID в URL и не выводите его в журнал.
Включённый session.use_strict_mode заставляет штатный обработчик отвергать неинициализированные идентификаторы. При успешном входе и изменении уровня привилегий идентификатор меняют, чтобы ранее известный ID не получил новые права. Немедленное удаление старой сессии при параллельных запросах может давать потерю состояния; для сложного приложения нужна продуманная миграция и краткий переходный период. Учебный последовательный пример показывает минимальную схему.
Проверить этот механизм в полном кабинете с настоящими HTTP-запросами.
Успешный пароль ещё не заканчивает вход
Проверка пароля и запись сессии должны сопровождаться политикой активности аккаунта. Заблокированный сотрудник не получает доступ только потому, что знает старый пароль. На чувствительных действиях проверяют текущие права, а не только роль, сохранённую полгода назад. Ограничение попыток учитывает аккаунт и источники запросов, но не должно позволять злоумышленнику навсегда заблокировать чужой вход.
Выход — изменение состояния, поэтому это POST с CSRF. Очистите данные сессии, удалите cookie с теми же path/domain и завершите серверную сессию. session_destroy не удаляет автоматически cookie браузера и не очищает текущий массив $_SESSION. Для выхода со всех устройств нужна отдельная серверная модель активных сессий или версия авторизации, сверяемая при запросе.
Проверить этот механизм в полном кабинете с настоящими HTTP-запросами.
Почему соседний AJAX ждёт и куда поставить session_write_close
Стандартное файловое хранилище блокирует запись сессии между session_start и её закрытием. Если обработчик десять секунд ждёт API, второй запрос того же браузера может ждать ту же блокировку. Отмена fetch в браузере не гарантирует остановку серверного скрипта. Поэтому прочитайте нужный user_id, сохраните изменения и вызовите session_write_close до медленной работы. Поведение других обработчиков зависит от их механизма блокировок.
После закрытия $_SESSION остаётся доступным в памяти, но присваивания в него уже не сохраняются автоматически. Если затем открыть сессию повторно, данные могли измениться другим запросом: нельзя бездумно перезаписать их старой копией. Длительный расчёт лучше хранить в задании базы/очереди, а сессию использовать лишь для определения пользователя. Для чтения есть read_and_close, но учитывайте особенности обновления времени хранения конкретным обработчиком.
Сессия не является универсальной блокировкой заказа. Два разных пользователя могут купить последний экземпляр; их сессии независимы. Такие гонки решают ограничениями и атомарными операциями базы. В то же время увеличение числа PHP-FPM процессов не устранит последовательное ожидание одной файловой сессии.
- session_start: прочитать пользователя
- Записать необходимые изменения
- session_write_close: снять блокировку
- Долгий запрос API / расчёт
- Ответ без повторного изменения старой копии
Сроки жизни и эксплуатация
Срок cookie, время простоя, максимальная длительность входа и удаление серверного файла — разные механизмы. Не полагайтесь исключительно на случайную уборку gc: приложение должно явно проверять собственные last_seen_at и authenticated_at. Перед обновлением last_seen_at сначала вычислите, не истёк ли допустимый интервал. Для банковских и административных операций нужна более строгая политика повторного подтверждения, чем для корзины.
При нескольких серверах общее хранилище сессий или осознанная привязка к узлу обязательны. Локальный файл на первом сервере не появится на втором. Потеря Redis/файлового хранилища должна приводить к контролируемой ошибке входа, а не к созданию псевдоавторизованного состояния. Проверяйте параллельные вкладки, повторный вход, выход, истечение времени и смену прав отдельными сценариями.
Корзина, привилегии и конкурентные вкладки
Корзина в сессии удобна для небольшого сайта, но хранить там окончательную цену заказа нельзя. При оформлении перечитайте цены, доступность и допустимые скидки. Если пользователь вошёл с другого устройства, синхронизация корзины уже требует серверной модели с версией и понятными правилами объединения. Сессия описывает текущий вход, а не автоматически все данные клиента.
При смене пароля или блокировке аккаунта заранее решите, что происходит с остальными активными сессиями. Можно хранить версию авторизации в аккаунте и сравнивать её с версией входа либо вести таблицу сессий с отзывом. Истечение browser-cookie не гарантирует немедленное уничтожение серверного файла, а наличие файла не означает действующие полномочия.
Отдельно проверяйте вход в двух вкладках, logout во время долгого запроса и повторный запуск после смены ID. Код, который безусловно записывает старый user_id после внешнего API, может восстановить уже завершённое состояние. Такие сценарии хорошо показывают, почему долгую бизнес-операцию и жизненный цикл сессии стоит разделять.
Практика: проверьте решение, а не только успешный путь
Отчёт читает user_id, вызывает API 8 секунд и в конце пишет flash. Как убрать блокировку, не потеряв flash?
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
session_startЗагрузить корзину этого посетителя в $_SESSION или начать новую сессию.
session_start(array $options = []): boolСохранить доставку для следующего открытия. Вызов без аргументов открывает сессию; $_SESSION['delivery'] выбирает ячейку «способ доставки». Присваивание = меняет её в памяти. Закрытие сохраняет её, следующий start читает сохранённое.
- delivery=Самовывоз записано в $_SESSION
- write_close сохраняет → новый start читает
- Страница получает: Доставка: Самовывоз
Распакуйте все файлы архива вместе. Команда php example.php выполняет этот пример; файлы из архива не нужно придумывать самостоятельно.
require __DIR__ . '/bootstrap.php' выполняет приложенный общий файл: __DIR__ — папка примера, точка соединяет путь. Он создаёт собственную session-data, выбирает files/php_serialize и отключает cookie только для CLI. Cookie между настоящими HTTP-запросами показана отдельно.
Загрузить корзину этого посетителя в $_SESSION или начать новую сессию.
| Часть вызова | Что означает |
|---|---|
| options | Массив переопределений session.* без префикса session.: например use_strict_mode. Отдельный read_and_close читает и сразу закрывает. |
| Результат и отказы | true при успешном старте; false и предупреждение при ошибке открытия/чтения. С PHP7.1 неуспешный старт не инициализирует $_SESSION. Повторный старт активной сессии не начинает вторую. |
| PHP8.5 | Числовой ключ options вызывает ValueError. Значения options принимают string/int/bool; иной тип даёт TypeError. Для read_and_close строка обязана представлять целое: положительное значение включает чтение/закрытие,0 и отрицательное выключают. Используйте настоящий bool. |
| HTTP-порядок | До вывода тела: PHP может отправить Set-Cookie и cache-заголовки. Настройка имени/хранилища/обработчика предшествует старту. |
<?php
declare(strict_types=1);
require __DIR__ . '/bootstrap.php';
if (!session_start()) { throw new RuntimeException('Сессия недоступна'); }
$_SESSION['delivery'] = 'Самовывоз';
session_write_close();
session_start();
$delivery = $_SESSION['delivery'];
session_write_close();
echo 'Доставка: ', $delivery, "\n";
Ожидаемый результат
Доставка: Самовывоз
В архиве bootstrap.php создаёт только свою session-data, выбирает files/php_serialize и отключает cookie для CLI. Повторные start в одном запуске проверяют запись и чтение backend; это не имитация доставки cookie. В браузере тот же путь данных выполняют два HTTP-запроса в полном проекте входа. Полученные строки показывают конкретное сохранённое значение или действие; ID посетителя в вывод не попадает.
read_and_close не запрещает менять массив в памяти, но эти изменения не сохраняются. У файлового обработчика такой проход не обновляет mtime: учитывайте политику удаления.
<?php
require __DIR__ . '/bootstrap.php';
session_start();
$_SESSION['cart_qty'] = 2;
session_write_close();
session_start(['read_and_close' => true]);
$_SESSION['cart_qty'] = 99;
session_start();
$saved = $_SESSION['cart_qty'];
session_write_close();
echo $saved, "
";
Ожидаемый результат
2
Это отдельный тест PHP8.5+. foreach перебирает два массива; catch перехватывает названные ошибки. Он не изображает отказ диска как ошибку параметров.
<?php
require __DIR__ . '/bootstrap.php';
$errors = [];
foreach ([[123 => true], ['read_and_close' => []]] as $options) {
try { session_start($options); }
catch (ValueError|TypeError $error) { $errors[] = $error::class; }
}
echo implode(',', $errors), "
";
Ожидаемый результат
ValueError,TypeError
Встроенные обработчики буферизации тоже имеют порядок: если используется ob_gzhandler, его регистрируют до старта; PHP рекомендует zlib.output_compression. Автоматическое переписывание URL связано с устаревающим trans_sid, а не с обычной cookie-сессией.
Дальше: session_status; session_write_close; session_set_cookie_params.
Читает идентификатор cookie и вызывает настроенный обработчик хранения. Возвращает true при успешном запуске. Options применяются к текущему запуску; session.auto_start и уже открытая сессия требуют согласованного поведения.
Рядом по задаче. session_status проверяет состояние; session_write_close сохраняет и освобождает блокировку.
session_statusПонять, можно ли сейчас сохранять изменения сессии.
session_status(): intРазрешать запись сообщения только в открытую сессию. Функция ничего не открывает. Константа PHP_SESSION_ACTIVE означает, что PHP сейчас работает с хранилищем. Это не признак входа пользователя.
- Сессия ACTIVE → сохраняем notice=Адрес сохранён
- write_close переводит в NONE → позднюю запись отклоняем
- Следующее открытие читает notice=Адрес сохранён
Распакуйте все файлы архива вместе. Команда php example.php выполняет этот пример; файлы из архива не нужно придумывать самостоятельно.
require __DIR__ . '/bootstrap.php' выполняет приложенный общий файл: __DIR__ — папка примера, точка соединяет путь. Он создаёт собственную session-data, выбирает files/php_serialize и отключает cookie только для CLI. Cookie между настоящими HTTP-запросами показана отдельно.
Понять, можно ли сейчас сохранять изменения сессии.
| Часть вызова | Что означает |
|---|---|
| Аргументы | Нет. |
| Результат | PHP_SESSION_DISABLED, PHP_SESSION_NONE или PHP_SESSION_ACTIVE. Сравнивайте именованные константы, а не магические числа. |
| Граница | ACTIVE означает открытую сессию, а не успешный вход пользователя; проверка user_id и прав отдельная. Непустой session_id может остаться после закрытия. |
<?php
declare(strict_types=1);
require __DIR__ . '/bootstrap.php';
session_start();
if (session_status() === PHP_SESSION_ACTIVE) { $_SESSION['notice'] = 'Адрес сохранён'; }
session_write_close();
if (session_status() !== PHP_SESSION_ACTIVE) { echo "Запись закрыта; поздние изменения не сохраняем.\n"; }
session_start(); $notice = $_SESSION['notice']; session_write_close();
echo 'Сообщение следующей страницы: ', $notice, "\n";
Ожидаемый результат
Запись закрыта; поздние изменения не сохраняем.
Сообщение следующей страницы: Адрес сохранён
В архиве bootstrap.php создаёт только свою session-data, выбирает files/php_serialize и отключает cookie для CLI. Повторные start в одном запуске проверяют запись и чтение backend; это не имитация доставки cookie. В браузере тот же путь данных выполняют два HTTP-запроса в полном проекте входа. Полученные строки показывают конкретное сохранённое значение или действие; ID посетителя в вывод не попадает.
void означает, что функция выполняет действие без возвращаемого значения. Защитная проверка обнаруживает запись после освобождения блокировки.
<?php
require __DIR__ . '/bootstrap.php';
function addNotice(string $text): void {
if (session_status() !== PHP_SESSION_ACTIVE) { throw new LogicException('Session is closed'); }
$_SESSION['notice'] = $text;
}
session_start();
addNotice('Saved');
session_write_close();
try { addNotice('Too late'); }
catch (LogicException $error) { echo $error->getMessage(), "
"; }
Ожидаемый результат
Session is closed
Дальше: session_start; session_id.
Возвращает PHP_SESSION_DISABLED, PHP_SESSION_NONE либо PHP_SESSION_ACTIVE. Это состояние механизма сессий, а не подтверждение входа пользователя.
Рядом по задаче. session_start открывает сессию, session_write_close переводит активную сессию в закрытую.
session_write_closeСохранить текущие данные и освободить сессию для соседних запросов.
session_write_close(): boolСохранить корзину перед долгой подготовкой страницы. У files открытая сессия удерживает блокировку одного ID. Сохраняем количество и освобождаем её до дальнейшей работы. Локальная переменная остаётся доступна после закрытия.
- В памяти cart_qty=3
- write_close записывает3 и освобождает files lock
- Повторное чтение: кисть —3шт.
Распакуйте все файлы архива вместе. Команда php example.php выполняет этот пример; файлы из архива не нужно придумывать самостоятельно.
require __DIR__ . '/bootstrap.php' выполняет приложенный общий файл: __DIR__ — папка примера, точка соединяет путь. Он создаёт собственную session-data, выбирает files/php_serialize и отключает cookie только для CLI. Cookie между настоящими HTTP-запросами показана отдельно.
Сохранить текущие данные и освободить сессию для соседних запросов.
| Часть вызова | Что означает |
|---|---|
| Аргументы | Нет. Вызывать после последнего изменения $_SESSION, до долгого API-запроса/рендеринга. |
| Результат | true при закрытии активной сессии; false, если активной сессии нет. Bool у write_close с PHP7.2 (раньше void); commit — alias с PHP4.4. |
| Граница гарантии | В PHP8.5.11 true не удостоверяет успешную долговечную запись: внутренний write может предупредить об отказе, а функция всё равно вернуть true. Не делайте сессию единственным журналом платежа/заказа. |
| После закрытия | $_SESSION остаётся доступен в памяти. Изменения без нового старта не попадут в хранилище; блокировка files уже освобождена. Это не завершает весь HTTP-ответ и не отправляет его немедленно. |
<?php
declare(strict_types=1);
require __DIR__ . '/bootstrap.php';
session_start(); $_SESSION['cart_qty'] = 3;
$qty = $_SESSION['cart_qty'];
session_write_close();
echo 'Страница заказа: кисть — ', $qty, " шт.\n";
session_start(); $saved = $_SESSION['cart_qty']; session_write_close();
echo 'Следующий запрос: кисть — ', $saved, " шт.\n";
Ожидаемый результат
Страница заказа: кисть — 3 шт.
Следующий запрос: кисть — 3 шт.
В архиве bootstrap.php создаёт только свою session-data, выбирает files/php_serialize и отключает cookie для CLI. Повторные start в одном запуске проверяют запись и чтение backend; это не имитация доставки cookie. В браузере тот же путь данных выполняют два HTTP-запроса в полном проекте входа. Полученные строки показывают конкретное сохранённое значение или действие; ID посетителя в вывод не попадает.
Второе закрытие false не записывает устаревшую локальную копию. Для реальной базы авторизация и изменения заказа выполняются отдельно.
<?php
require __DIR__ . '/bootstrap.php';
session_start();
$_SESSION['notice'] = 'Saved';
session_write_close();
$_SESSION['notice'] = 'Only in memory';
$again = session_write_close();
session_start();
$saved = $_SESSION['notice'];
session_write_close();
var_dump($again);
echo $saved, "\n";
Ожидаемый результат
bool(false)
Saved
Дальше: session_abort; session_reset; session_start.
Закрывает активную сессию и возвращает успешность. При обычном файловом обработчике освобождает блокировку. Массив $_SESSION остаётся в памяти.
Рядом по задаче. read_and_close у session_start подходит только чтению; session_abort закрывает без сохранения изменений.
session_regenerate_idСменить ключ текущего состояния, например перед назначением прав после входа.
session_regenerate_id(bool $delete_old_session = false): boolПередать права новому ID, оставив старую запись гостевой. false сохраняет прежнюю запись, что избегает немедленного удаления при соседнем запросе. Вход записывается только после смены ID. Здесь имитируется уже успешная проверка пароля; полный проект выполняет её в POST-обработчике.
- Старый ID: гость, cart=Кисть:2
- regenerate_id(false) → новый ID; затем user_id7
- Старый ключ остаётся гостевым, новый хранит пользователя7 и корзину
Распакуйте все файлы архива вместе. Команда php example.php выполняет этот пример; файлы из архива не нужно придумывать самостоятельно.
require __DIR__ . '/bootstrap.php' выполняет приложенный общий файл: __DIR__ — папка примера, точка соединяет путь. Он создаёт собственную session-data, выбирает files/php_serialize и отключает cookie только для CLI. Cookie между настоящими HTTP-запросами показана отдельно.
Сменить ключ текущего состояния, например перед назначением прав после входа.
| Часть вызова | Что означает |
|---|---|
| delete_old_session | false сохраняет старую запись; true вызывает её удаление. Оба варианта меняют ID, сохраняя текущий массив. |
| Результат | true/false; активная сессия и возможность отправить заголовки обязательны. При отказе не назначайте новые права. |
| Правильный момент | После проверки пароля/MFA, но до записи нового user_id/уровня доступа. strict mode не заменяет ротацию уже существующего известного ID. |
| Гонки | Не ротируйте на каждом AJAX. false сам по себе оставляет старую авторизацию: нужны retired_at/сроки и проверка на каждом защищённом запросе. true способен потерять состояние параллельного запроса или новой cookie при обрыве сети. |
<?php
declare(strict_types=1);
require __DIR__ . '/bootstrap.php';
session_start(); $_SESSION['cart'] = 'Кисть:2'; $guest = session_id();
$_SESSION['retired_at'] = 1000;
if (!session_regenerate_id(false)) { throw new RuntimeException('Не удалось сменить ID'); }
unset($_SESSION['retired_at']); $_SESSION['user_id'] = 7;
$member = session_id(); session_write_close();
session_id($guest); session_start(); $old = $_SESSION['user_id'] ?? null; session_write_close();
session_id($member); session_start(); $cart = $_SESSION['cart']; $user = $_SESSION['user_id']; session_write_close();
echo $old === null ? "Старый ID: гость\n" : "Ошибка прав\n";
echo 'Новый ID: пользователь ', $user, '; корзина ', $cart, "\n";
Ожидаемый результат
Старый ID: гость
Новый ID: пользователь 7; корзина Кисть:2
В архиве bootstrap.php создаёт только свою session-data, выбирает files/php_serialize и отключает cookie для CLI. Повторные start в одном запуске проверяют запись и чтение backend; это не имитация доставки cookie. В браузере тот же путь данных выполняют два HTTP-запроса в полном проекте входа. Полученные строки показывают конкретное сохранённое значение или действие; ID посетителя в вывод не попадает.
Вместо проверки пароля здесь заранее задан факт успеха; 1000 — учебное время. Защищённый контроллер обязан проверять retired_at и сроки, а не считать любую загруженную сессию действующей.
<?php
require __DIR__ . '/bootstrap.php';
session_start(); $_SESSION['cart_qty'] = 2;
$old = session_id(); $_SESSION['retired_at'] = 1000;
if (!session_regenerate_id(false)) { throw new RuntimeException('Rotation failed'); }
unset($_SESSION['retired_at']); $_SESSION['user_id'] = 42; session_write_close();
session_id($old); session_start();
$oldHasLogin = isset($_SESSION['user_id']); $retired = $_SESSION['retired_at'];
session_write_close(); var_dump($oldHasLogin); echo $retired, "
";
Ожидаемый результат
bool(false)
1000
Глава Безопасность и сроки разделяет гостевую ротацию, повышение прав, плановую ротацию уже авторизованной сессии и отзыв всех устройств. Нельзя представлять две строки regenerate_id(true) как полноценный менеджер конкурентных сессий.
Дальше: session_create_id; session_destroy; session_start.
Создаёт новый ID, сохраняя текущие данные. true просит удалить старые данные. Возвращает bool; требуется активная сессия и возможность отправить cookie.
Рядом по задаче. session_start с use_strict_mode дополняет защиту. Полный сценарий входа.
session_destroyУдалить запись текущей сессии из её серверного хранилища.
session_destroy(): boolУдалить серверное состояние и перестать использовать старую память. destroy удаляет запись обработчика. Отдельно очищаем $_SESSION, чтобы текущий код не продолжил читать старый user_id. Cookie браузера удаляет HTTP-выход из полного проекта.
- Сохранена запись с user_id7
- destroy удаляет запись; $_SESSION=[] очищает память
- Старый ID при strict mode не восстанавливает вход
Распакуйте все файлы архива вместе. Команда php example.php выполняет этот пример; файлы из архива не нужно придумывать самостоятельно.
require __DIR__ . '/bootstrap.php' выполняет приложенный общий файл: __DIR__ — папка примера, точка соединяет путь. Он создаёт собственную session-data, выбирает files/php_serialize и отключает cookie только для CLI. Cookie между настоящими HTTP-запросами показана отдельно.
Удалить запись текущей сессии из её серверного хранилища.
| Часть вызова | Что означает |
|---|---|
| Аргументы | Нет; нужна активная сессия. |
| Результат | true при успехе, false при отказе; без активной сессии предупреждение. |
| Не делает автоматически | Не очищает $_SESSION в памяти и cookie браузера. Для выхода очищайте данные/снимайте авторизацию и отдельно истекайте cookie с теми же именем, path и domain. |
| Параллельные запросы | Мгновенное удаление активной записи даёт гонки. Для приложения с несколькими вкладками нужны отметка отзыва, серверные сроки и согласованная политика — подробно о ротации. |
<?php
declare(strict_types=1);
require __DIR__ . '/bootstrap.php';
session_start(); $_SESSION['user_id'] = 7;
$id = session_id(); session_write_close();
session_start(); session_destroy(); $_SESSION = [];
session_id($id); session_start();
$message = isset($_SESSION['user_id']) ? 'Доступ разрешён' : 'Нужен новый вход';
session_write_close(); echo $message, "\n";
Ожидаемый результат
Нужен новый вход
В архиве bootstrap.php создаёт только свою session-data, выбирает files/php_serialize и отключает cookie для CLI. Повторные start в одном запуске проверяют запись и чтение backend; это не имитация доставки cookie. В браузере тот же путь данных выполняют два HTTP-запроса в полном проекте входа. Полученные строки показывают конкретное сохранённое значение или действие; ID посетителя в вывод не попадает.
Файловый обработчик со strict mode отказывается усыновить идентификатор, записи которого уже нет; новая сессия пуста.
<?php
require __DIR__ . '/bootstrap.php';
session_start(); $_SESSION['cart_qty'] = 2; session_write_close();
session_start(); $old = session_id(); session_destroy(); $_SESSION = [];
session_id($old); session_start();
$new = session_id() !== $old; $empty = $_SESSION === []; session_write_close();
var_dump($new, $empty);
Ожидаемый результат
bool(true)
bool(true)
Дальше: session_unset; session_regenerate_id; session_get_cookie_params.
Уничтожает данные настроенного хранилища для текущего ID и возвращает bool. Не очищает автоматически $_SESSION и cookie браузера.
Рядом по задаче. session_unset очищает переменные активной сессии; setcookie с истёкшим сроком удаляет cookie.
session_set_cookie_paramsНастроить область и ограничения session-cookie перед её отправкой.
session_set_cookie_params(array|int $lifetime_or_options, ?string $path = null, ?string $domain = null, ?bool $secure = null, ?bool $httponly = null): boolЗадать политику cookie до открытия сессии. Массив options связывает имя настройки с её значением через =>. lifetime0 означает сессионную cookie, не бесконечный вход. Здесь проверяется конфигурация HTTPS; фактический localhost HTTP показан в проекте.
- До start задано path/, Secure, HttpOnly, Lax
- get_cookie_params возвращает эффективные настройки
- Конфигурация разрешает передачу только через HTTPS
Распакуйте все файлы архива вместе. Команда php example.php выполняет этот пример; файлы из архива не нужно придумывать самостоятельно.
require __DIR__ . '/bootstrap.php' выполняет приложенный общий файл: __DIR__ — папка примера, точка соединяет путь. Он создаёт собственную session-data, выбирает files/php_serialize и отключает cookie только для CLI. Cookie между настоящими HTTP-запросами показана отдельно.
Настроить область и ограничения session-cookie перед её отправкой.
| Часть вызова | Что означает |
|---|---|
| lifetime_or_options | Число секунд или массив (с PHP7.3). Массив: lifetime/path/domain/secure/httponly/samesite, с8.5 partitioned. Не смешивать массив с дополнительными позиционными параметрами. |
| lifetime/path/domain | 0 — сессионная cookie; / — весь сайт; пустой domain — только текущий host. Это не время серверной авторизации. Режим восстановления браузера может восстановить session-cookie. |
| secure/httponly | Secure ограничивает HTTPS; HttpOnly запрещает чтение JavaScript, но не устраняет XSS-действия с cookie. Пропущенные/null параметры сохраняют настройки; nullable с8.0. |
| samesite/partitioned | Lax/Strict выбирают cross-site-политику; None применяется для нужных cross-site сценариев с Secure. Partitioned8.5 изолирует cookie по верхнему сайту и требует Secure; поддержка браузера отдельна. |
| Результат | bool (с7.2, ранее void); только текущий запрос и до start/вывода. Требует use_cookies=1; при отключённых cookie возвращает false с предупреждением даже в CLI. Некорректные опции предупреждают/ошибаются; пустой массив и смешанные формы в8.5.11 дают ValueError. |
<?php
declare(strict_types=1);
require __DIR__ . '/bootstrap.php';
ini_set('session.use_cookies', '1');
session_set_cookie_params(['lifetime' => 0, 'path' => '/', 'secure' => true, 'httponly' => true, 'samesite' => 'Lax']);
$policy = session_get_cookie_params();
echo 'Область: ', $policy['path'], "\n";
echo $policy['secure'] ? "Передача: HTTPS\n" : "Передача: HTTP тоже разрешён\n";
echo $policy['httponly'] ? "JavaScript не читает cookie.\n" : "Cookie доступна JavaScript.\n";
echo 'SameSite: ', $policy['samesite'], "\n";
Ожидаемый результат
Область: /
Передача: HTTPS
JavaScript не читает cookie.
SameSite: Lax
В архиве bootstrap.php создаёт только свою session-data, выбирает files/php_serialize и отключает cookie для CLI. Повторные start в одном запуске проверяют запись и чтение backend; это не имитация доставки cookie. В браузере тот же путь данных выполняют два HTTP-запроса в полном проекте входа. Полученные строки показывают конкретное сохранённое значение или действие; ID посетителя в вывод не попадает.
Непереданные поля не сбрасываются: короткая настройка lifetime сохраняет предыдущий path и Secure.
<?php
require __DIR__ . '/bootstrap.php';
ini_set('session.use_cookies', '1');
session_set_cookie_params(600, '/account/', '', true, true);
session_set_cookie_params(['lifetime' => 0]);
$policy = session_get_cookie_params();
echo $policy['lifetime'], "
", $policy['path'], "
"; var_dump($policy['secure']);
Ожидаемый результат
0
/account/
bool(true)
Это опция для осмысленного встроенного cross-site-сценария, не универсальное улучшение обычной корзины.
<?php
require __DIR__ . '/bootstrap.php';
ini_set('session.use_cookies', '1');
session_set_cookie_params(['secure' => true, 'httponly' => true, 'samesite' => 'None', 'partitioned' => true]);
$policy = session_get_cookie_params(); var_dump($policy['partitioned'], $policy['secure']);
Ожидаемый результат
bool(true)
bool(true)
Дальше: session_get_cookie_params; session_start.
Вариант с массивом принимает lifetime, path, domain, secure, httponly, samesite. Настройка действует для текущего скрипта и должна предшествовать session_start.
Рядом по задаче. Те же параметры можно передать session_start с префиксом cookie_. setcookie нужен для обычных cookies.