Menu

Сессии и cookie в PHP: session_start и setcookie

Вызовите session_start() в начале каждой страницы и храните значения в $_SESSION['key']; PHP держит их на сервере и узнаёт посетителя по cookie. setcookie('name', 'value', $options) сохраняет небольшое значение в браузере, а прочитать его можно из $_COOKIE. Вход, выход, flash-сообщения, параметры cookie и настройки безопасности.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Вызовите session_start() в начале страницы, а потом читайте и пишите $_SESSION['key'], как любой массив: PHP сохраняет его на сервере и находит снова при следующем запросе посетителя. Для небольшого значения, которое должен хранить браузер, вызовите setcookie('name', 'value', time() + 86400) и читайте его в последующих запросах из $_COOKIE['name'].

<?php
session_start();                        // before any output
setcookie('theme', 'dark', time() + 60 * 60 * 24 * 30);   // 30 days, also before output

$_SESSION['visits'] = ($_SESSION['visits'] ?? 0) + 1;
$theme = $_COOKIE['theme'] ?? 'light';  // set on an earlier visit
echo "You have opened this page {$_SESSION['visits']} times.";

Сессии и cookie имеют смысл только на протяжении нескольких HTTP-запросов из браузера, поэтому примеры, которым они нужны, записаны обычным кодом. Логика вокруг них это обычный PHP с массивами, и эта часть здесь выполняется: в каждом запускаемом блоке $session заменяет $_SESSION, а каждый вызов это один запрос.

Как работает сессия PHP

  1. Первый session_start() создаёт случайный ID, отправляет его браузеру в cookie с именем PHPSESSID и даёт вам пустой $_SESSION.
  2. В конце запроса PHP сохраняет $_SESSION на сервере (по умолчанию в файлах, либо в Redis или базе данных).
  3. При следующем запросе браузер отправляет cookie обратно, session_start() читает ID и загружает сохранённый массив.

Браузер никогда не видит данные, только ID. Поэтому ID вошедшего пользователя место в $_SESSION, а не в вашей собственной cookie: посетитель может поменять свои cookie на любое значение, но не может поменять $_SESSION.

Вход и выход пользователя через сессию

Вход это проверить пароль, сменить ID сессии и сохранить ID пользователя. Выход это обратное действие, и в нём три шага.

<?php
// login.php
session_start();

$user = find_user_by_email($_POST['email'] ?? '');
if ($user && password_verify($_POST['password'] ?? '', $user['password_hash'])) {
    session_regenerate_id(true);        // new id after login: stops session fixation
    $_SESSION['user_id'] = $user['id'];
    header('Location: /dashboard.php');
    exit;
}
$error = 'Wrong email or password.';
<?php
// any protected page
session_start();
if (!isset($_SESSION['user_id'])) {
    header('Location: /login.php');
    exit;
}
<?php
// logout.php
session_start();
$_SESSION = [];                                     // 1. forget the data

$p = session_get_cookie_params();                   // 2. delete the cookie
setcookie(session_name(), '', time() - 3600, $p['path'], $p['domain'], $p['secure'], $p['httponly']);

session_destroy();                                  // 3. delete the stored session
header('Location: /');
exit;

session_regenerate_id(true) важнее, чем кажется. Если атакующий может заставить жертву использовать известный ему ID сессии (например, через ссылку), а ID после входа не меняется, атакующий тоже оказывается вошедшим. Новый ID при входе закрывает эту дыру. Сама проверка пароля описана на странице про password_hash.

Завершение сессии после бездействия

Собственная очистка PHP (session.gc_maxlifetime, по умолчанию 1440 секунд) запускается случайно и не является надёжным таймаутом. Храните время последнего запроса и проверяйте его сами. Блок использует фиксированные метки времени, чтобы был виден каждый случай; поменяйте $timeout или время.

Промежуток с 09:05 до 09:16:40 составляет 700 секунд, меньше 15-минутного лимита, поэтому пользователь остаётся в системе. Следующий промежуток 1000 секунд, поэтому сессия очищается.

Flash-сообщения

Flash-сообщение задаётся в одном запросе и показывается один раз в следующем, обычно "Сохранено." после редиректа. Храните его в сессии, а потом читайте и удаляйте за один шаг:

setcookie() принимает имя, значение и массив параметров (PHP 7.3 и новее). Задавайте эти четыре параметра у каждой cookie, если нет причины поступить иначе:

<?php
setcookie('theme', 'dark', [
    'expires'  => time() + 60 * 60 * 24 * 30,   // 30 days; 0 means "until the browser closes"
    'path'     => '/',                          // send it on every page, not just this folder
    'secure'   => true,                         // only over HTTPS
    'httponly' => true,                         // JavaScript cannot read it
    'samesite' => 'Lax',                        // not sent on most cross-site requests
]);

$theme = $_COOKIE['theme'] ?? 'light';

На двух вещах часто спотыкаются:

  • $_COOKIE не меняется в том же запросе. Он содержит то, что браузер прислал с этим запросом. Cookie, которую вы устанавливаете сейчас, появится в $_COOKIE только в следующем.
  • Cookie это заголовки, поэтому setcookie() должна выполняться до любого вывода, ровно как session_start() и header().

Чтобы удалить cookie, установите её снова с тем же именем и path и сроком действия в прошлом: setcookie('theme', '', ['expires' => time() - 3600, 'path' => '/']).

Значение cookie это строка не длиннее примерно 4 КБ, поэтому структурированные данные храните в JSON и считайте то, что вернулось, недоверенным: посетитель может это изменить. Никогда не делайте unserialize() для cookie: так можно создать объекты ваших собственных классов и запустить их магические методы, а json_decode() возвращает только массивы, строки, числа, логические значения, null или stdClass.

Флаги описаны на странице про json_encode. Для всего, что больше нескольких коротких значений, держите данные в сессии (или в базе данных), а в cookie храните только ключ.

Токены "запомнить меня"

Cookie "запомнить меня" не должна хранить ID пользователя или хеш пароля. Храните в cookie длинный случайный токен, а в базе данных только его хеш, и сравнивайте через hash_equals():

Значения меняются при каждом запуске, потому что random_bytes() случайна. Если база данных утечёт, хеши бесполезны без cookie, а hash_equals() сравнивает за постоянное время, поэтому проверка не выдаёт информацию через время выполнения.

Сессия ($_SESSION)Cookie ($_COOKIE)
Где хранитсяНа сервереВ браузере
Посетитель может прочитать или изменитьНет (только ID)Да
РазмерОграничен вашим хранилищемОколо 4 КБ на cookie
Сколько живётДо закрытия браузера или истечения сессииДо времени expires
Для чегоВошедший пользователь, корзина, CSRF-токен, flash-сообщенияТема, язык, выбор согласия, токен "запомнить меня"

Частая ошибка: headers already sent

session_start(), setcookie() и header() отправляют HTTP-заголовки, а заголовки можно отправить только до тела ответа. Любой вывод раньше, даже пустая строка перед <?php в подключённом файле, приводит к:

Warning: session_start(): Session cannot be started after headers have already been sent in /var/www/page.php on line 3
Warning: Cannot modify header information - headers already sent by (output started at /var/www/page.php:2) in /var/www/page.php on line 4

Второе сообщение говорит, где начался вывод (page.php:2): перейдите к этой строке и уберите вывод или переместите session_start() выше. В файлах, где только PHP, отсутствие закрывающего ?> не даёт случайным пробелам после него стать выводом.

Часто задаваемые вопросы

Чем сессия отличается от cookie в PHP?

Cookie хранится в браузере посетителя и отправляется с каждым запросом, поэтому посетитель может её прочитать и изменить. Сессия хранит данные на сервере; браузер держит в cookie только случайный ID сессии (PHPSESSID). Всё, чему нужно доверять, например ID вошедшего пользователя, кладите в $_SESSION.

Как уничтожить сессию при выходе в PHP?

Очистите данные, удалите cookie сессии и уничтожьте сохранённую сессию: $_SESSION = [];, затем setcookie(session_name(), '', time() - 3600, '/'), затем session_destroy();. После этого сделайте редирект, чтобы следующая страница начиналась с чистого листа.

Как удалить cookie в PHP?

Установите её снова с тем же именем, путём и доменом и сроком действия в прошлом: setcookie('theme', '', ['expires' => time() - 3600, 'path' => '/']);. Сделайте ещё и unset($_COOKIE['theme']), если её читает остаток текущего скрипта, потому что $_COOKIE меняется только при следующем запросе.

Сколько живёт сессия PHP?

По умолчанию cookie сессии живёт до закрытия браузера (session.cookie_lifetime = 0), а сервер может удалить данные сессии через 1440 секунд (24 минуты) бездействия (session.gc_maxlifetime). Для фиксированного таймаута храните в сессии время последнего запроса и проверяйте его сами.

Почему session_start() пишет headers already sent?

ID сессии передаётся в заголовке cookie, а заголовки должны быть отправлены до любого вывода. echo, HTML или даже пустая строка или BOM перед <?php отправляют вывод первыми. Вызывайте session_start() в самом начале скрипта, до того как что-либо выведено.

Иллюстрация языков программирования Coddy

Учитесь программировать с Coddy

НАЧАТЬ