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

Знакомая картина: вы написали форму входа, положили $_SESSION['user_id'] и радуетесь. Но при переходе на соседнюю страницу пользователь снова видит форму логина, будто и не входил. Или в логах всплывает headers already sent, и сессия вообще не стартует. Через полчаса этой инструкции у вас будет рабочая аутентификация на сессиях, которая переживает переход между страницами, и понимание, почему предыдущие попытки разваливались. Всё написано для PHP 8.x.
Разберём типичные грабли по порядку: как сессия вообще работает, почему session_start() капризничает, чем куки отличаются от сессий и как не потерять данные при выходе.
Что нужно знать до начала
- PHP 8.0 или новее. Проверьте:
php -vв терминале. - Сервер с поддержкой PHP — встроенный
php -S localhost:8000подойдёт для локальной проверки. - Права на запись в каталог, где PHP хранит файлы сессий. По умолчанию это временная папка системы; путь виден в
session.save_pathвыводаphpinfo(). - Один принцип, который снимает половину проблем:
session_start()должен вызываться до любого вывода — до HTML, доecho, даже до пробела перед открывающим<?php.
Как это устроено в двух предложениях
При session_start() PHP генерирует идентификатор сессии, кладёт его в куку PHPSESSID в браузере и создаёт на сервере файл с данными. При следующем запросе браузер присылает эту куку обратно, PHP находит файл по ID и наполняет $_SESSION. Кука хранит только ключ; сами данные лежат на сервере.
Сессия — это серверная память, привязанная к куке в браузере. Потеряете куку — потеряете сессию, даже если файл на сервере цел.
Пошагово: рабочая сессия
Стартуйте сессию первой строкой файла. Никакого HTML, никакого BOM, никакого
echoвыше.<?php // index.php — PHP 8.x session_start(); $_SESSION['visits'] = ($_SESSION['visits'] ?? 0) + 1; echo 'Вы открыли страницу ' . $_SESSION['visits'] . ' раз';Проверка: обновите страницу несколько раз. Счётчик должен расти. Если он всегда показывает 1 — сессия не сохраняется, смотрите раздел ниже.
Проверьте, что стартуете сессию только один раз за запрос. Повторный
session_start()в PHP 8 выдаётNotice: session already active. Обычно это случается, когда файл с сессией подключается черезincludeдважды. Защитите вызов:<?php if (session_status() === PHP_SESSION_NONE) { session_start(); }Записывайте в
$_SESSIONкак в обычный массив. Отдельно сохранять ничего не нужно — PHP сам запишет данные в файл в конце запроса.<?php if (session_status() === PHP_SESSION_NONE) { session_start(); } // после успешной проверки логина и пароля $_SESSION['user_id'] = 42; $_SESSION['login'] = 'ivan';Проверяйте авторизацию на защищённых страницах. На каждой странице, где нужен вход, сначала стартуйте сессию, потом смотрите, есть ли
user_id.<?php if (session_status() === PHP_SESSION_NONE) { session_start(); } if (empty($_SESSION['user_id'])) { header('Location: /login.php'); exit; // без exit скрипт выполнится дальше, несмотря на редирект }Проверка: откройте защищённую страницу без входа — вас должно перекинуть на логин.
Выход делайте в три шага, а не одним
unset. Очистите массив, удалите файл сессии, погасите куку.<?php if (session_status() === PHP_SESSION_NONE) { session_start(); } $_SESSION = []; // удалить куку сессии в браузере if (ini_get('session.use_cookies')) { $params = session_get_cookie_params(); setcookie( session_name(), '', time() - 42000, $params['path'], $params['domain'], $params['secure'], $params['httponly'] ); } session_destroy(); header('Location: /login.php'); exit;Проверка: после выхода защищённая страница снова перекидывает на логин, а в инструменте разработчика браузера куки
PHPSESSIDбольше нет либо она пустая.
Куки отдельно от сессий
Не путайте $_SESSION и setcookie(). Кука через setcookie() — это данные, которые хранятся в браузере и целиком приходят на сервер с каждым запросом. Годятся для настроек вроде темы оформления, но не для секретов: пользователь их видит и может изменить.
<?php
// Кука живёт 30 дней, доступна на всём сайте, только по HTTPS,
// недоступна из JavaScript (защита от кражи через XSS)
setcookie('theme', 'dark', [
'expires' => time() + 30 * 24 * 60 * 60,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);Важный нюанс: значение, которое вы только что задали через setcookie(), появится в $_COOKIE только при следующем запросе, а не сразу в этом же скрипте. Синтаксис с массивом параметров работает начиная с PHP 7.3.
Флаги, которые новички забывают
| Флаг | Зачем | Что будет без него |
|---|---|---|
httponly | Кука недоступна из JavaScript | Скрипт с чужого домена (XSS) может украсть сессию |
secure | Кука уходит только по HTTPS | Идентификатор сессии могут перехватить по открытому HTTP |
samesite | Ограничивает отправку куки на сторонние запросы | Выше риск CSRF-атак |
Для сессионной куки эти флаги задают до session_start() через session_set_cookie_params() — но проверяйте актуальный синтаксис в документации вашей версии PHP, он различается для формы с массивом и старой формы с отдельными аргументами.
Если не получилось
Warning: session_start(): Cannot start session when headers already sent— доsession_start()что-то уже вывелось в браузер. Частая причина — пробел или пустая строка перед<?phpв подключаемом файле, либо BOM в начале файла. Уберите весь вывод выше старта сессии. В чистых PHP-файлах закрывающий?>в конце лучше вообще не ставить.Счётчик всегда 1, данные не сохраняются между запросами. Либо каталог
session.save_pathнедоступен для записи (смотрите права), либо браузер не принимает кукуPHPSESSID. Откройте инструменты разработчика, вкладка с куками — еслиPHPSESSIDнет, проблема в куке. Наlocalhostбез HTTPS кука с флагомsecureне сохранится.Notice: session already active—session_start()вызван повторно. Оберните вызов проверкойsession_status(), как в шаге 2.После выхода пользователь всё ещё «залогинен». Скорее всего, вы сделали только
unset($_SESSION['user_id'])илиsession_destroy(), но не погасили куку. Используйте полный вариант выхода из шага 5.Сессия слетает через несколько минут. Время жизни данных определяется
session.gc_maxlifetime, а время жизни куки —session.cookie_lifetime. Эти два параметра независимы; сверяйте их значения вphpinfo(), если сессия живёт не столько, сколько ожидаете.
Что делать дальше
Когда базовая аутентификация работает, добавьте вызов session_regenerate_id(true) сразу после успешного входа — это меняет идентификатор сессии и закрывает атаку с фиксацией сессии. Дальше стоит посмотреть в сторону хранения сессий не в файлах, а в Redis или в базе, если у вас несколько серверов за балансировщиком: файловые сессии на одном сервере другой сервер не увидит. Настройки хранилища задаются через session.save_handler — конкретный синтаксис для Redis смотрите в документации соответствующего расширения.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: PHP Manual: Session handling functions, PHP Manual: setcookie
Частые вопросы
Обязательно ли вызывать session_start() на каждой странице?
А если у меня PHP 7, код с массивом параметров в setcookie сработает?
Почему кука появляется в $_COOKIE только со второго запроса?
Можно ли хранить пароль или флаг is_admin в куке?
Сессия слетает при переходе между www.site.ru и site.ru — почему?
Не сработало на localhost, хотя код верный — в чём дело?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.