
Всем хорошего дня. Перед вами первая статья из серии PHP для начинающих разработчиков. Это будет необычная серия статей, тут не будет echo "Hello World"
, тут будет hardcore из жизни PHP программистов с небольшой примесью «домашней работы» для закрепления материала.
Начну с сессий — это один из самых важных компонентов, с которыми вам придется работать. Не понимая принципов его работы — наворотите делов. Так что во избежание проблем я постараюсь рассказать о всех возможных нюансах.
Но для начала, чтобы понять зачем нам сессия, обратимся к истокам — к HTTP протоколу.
HTTP Protocol
HTTP протокол — это HyperText Transfer Protocol — «протокол передачи гипертекста» — т.е. по сути — текстовый протокол, и его понять не составит труда.
Изначально подразумевали, что по этому протоколу будет только HTML передаваться, отсель и название, а сейчас чего только не отправляют и =^.^= и(•_ㅅ_•)
Чтобы не ходить вокруг да около, давайте я вам приведу пример общения по HTTP протоколу.
Вот пример запроса, каким его отправляет ваш браузер, когда вы запрашиваете страницу http://example.com
:
GET / HTTP/1.1
Host: example.com
Accept: text/html
<пустая строка>
А вот пример ответа:
HTTP/1.1 200 OK
Content-Length: 1983
Content-Type: text/html; charset=utf-8
<html>
<head>...</head>
<body>...</body>
</html>
Это очень упрощенные примеры, но и тут можно увидеть из чего состоят HTTP запрос и ответ:
- стартовая строка — для запроса содержит метод и путь запрашиваемой страницы, для ответа — версию протокола и код ответа
- заголовки — имеют формат ключ-значение разделенные двоеточием, каждый новый заголовок пишется с новой строки
- тело сообщения — непосредственно HTML либо данные отделяют от заголовков двумя переносами строки, могут отсутствовать, как в приведенном запросе
Так, вроде с протоколом разобрались — он простой, ведёт свою историю аж с 1992-го года, так что идеальным его не назовешь, но какой есть — отправили запрос — получите ответ, и всё, сервер и клиент никоим образом более не связаны. Но подобный сценарий отнюдь не единственный возможный, у нас же может быть авторизация, сервер должен каким-то образом понимать, что вот этот запрос пришёл от определенного пользователя, т.е. клиент и сервер должны общаться в рамках некой сессии. И да, для этого придумали следующий механизм:
- При авторизации пользователя, сервер генерирует и запоминает уникальный ключ — идентификатор сессии, и сообщает его браузеру
- Браузер сохраняет этот ключ, и при каждом последующем запросе, его отправляет
Для реализации этого механизма и были созданы cookie (куки, печеньки) — простые текстовые файлы на вашем компьютере, по файлу для каждого домена (хотя некоторые браузеры более продвинутые, и используют для хранения SQLite базу данных), при этом браузер накладывает ограничение на количество записей и размер хранимых данных (для большинства браузеров это 4096 байт, см. RFC 2109 от 1997-го года)
Т.е. если украсть cookie из вашего браузера, то можно будет зайти на вашу страничку в facebook от вашего имени? Не пугайтесь, так сделать нельзя, по крайней мере с facebook, и дальше я вам покажу один из возможных способов защиты от данного вида атаки на ваших пользователей.
Давайте теперь посмотрим как изменятся наши запрос-ответ, будь там авторизация:
Request
POST /login/ HTTP/1.1
Host: example.com
Accept: text/html
login=Username&password=Userpass
Метод у нас изменился на POST, и в теле запроса у нас передаются логин и пароль (если использовать метод GET, то строка запроса будет содержать логин и пароль, что не очень правильно с идеологической точки зрения, и имеет ряд побочных явлений в виде логирования и кеширования паролей в открытом виде).
Response
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Set-Cookie: KEY=VerySecretUniqueKey
<html>
<head>...</head>
<body>...</body>
</html>
Ответ сервер будет содержать заголовок Set-Cookie: KEY=VerySecretUniqueKey
, что заставит браузер сохранить эти данные в файлы cookie, и при следующем обращении к серверу — они будут отправлены и опознаны сервером:
Request
GET / HTTP/1.1
Host: example.com
Accept: text/html
Cookie: KEY=VerySecretUniqueKey
<пустая строка>
Как можно заметить, заголовки отправляемые браузером (Request Headers) и сервером (Response Headers) отличаются, хотя есть и общие и для запросов и для ответов (General Headers)
Сервер узнал нашего пользователя по присланным cookie, и дальше предоставит ему доступ к личной информации. Так, ну вроде с сессиями и HTTP разобрались, теперь можно вернутся к PHP и его особенностям.
PHP и сессия
Я надеюсь, у вас уже установлен PHP на компьютере, т.к. дальше я буду приводить примеры, и их надо будет запускать
Язык PHP создавался под стать протоколу HTTP — т.е. основная его задача — это дать ответ на HTTP запрос и «умереть» освободив память и ресурсы. Следовательно, и механизм сессий работает в PHP не в автоматическом режиме, а в ручном, и нужно знать что вызвать, да в каком порядке.
Вот вам статейка на тему PHP is meant to die, или вот она же на русском языке, но лучше отложите её в закладки «на потом».
Перво-наперво необходимо «стартовать» сессию — для этого воспользуемся функцией session_start(), создайте файл session.start.php со следующим содержимым:
<?php
session_start();
Запустите встроенный в PHP web-server в папке с вашим скриптом:
php -S 127.0.0.1:8080
Запустите браузер, и откройте в нём Developer Tools (или что там у вас), далее перейдите на страницу http://127.0.0.1:8080/session.start.php — вы должны увидеть лишь пустую страницу, но не спешите закрывать — посмотрите на заголовки которые нам прислал сервер:
Там будет много чего, интересует нас только вот эта строчка в ответе сервера (почистите куки, если нет такой строчки, и обновите страницу):
Set-Cookie: PHPSESSID=dap83arr6r3b56e0q7t5i0qf91; path=/
Увидев сие, браузер сохранит у себя куку с именем `PHPSESSID`:
PHPSESSID
— имя сессии по умолчанию, регулируется из конфига php.ini директивой session.name, при необходимости имя можно изменить в самом конфигурационном файле или с помощью функции session_name()
И теперь — обновляем страничку, и видим, что браузер отправляет эту куку на сервер, можете попробовать пару раз обновить страницу, результат будет идентичным:
Итого, что мы имеем — теория совпала с практикой, и это просто отлично.
Следующий шаг — сохраним в сессию произвольное значение, для этого в PHP используется супер-глобальная переменная $_SESSION
, сохранять будем текущее время — для этого вызовем функцию date():
session_start();
$_SESSION['time'] = date("H:i:s");
echo $_SESSION['time'];
Обновляем страничку и видим время сервера, обновляем ещё раз — и время обновилось. Давайте теперь сделаем так, чтобы установленное время не изменялось при каждом обновлении страницы:
session_start();
if (!isset($_SESSION['time'])) {
$_SESSION['time'] = date("H:i:s");
}
echo $_SESSION['time'];
Обновляем — время не меняется, то что нужно. Но при этом мы помним, PHP умирает, значит данную сессию он где-то хранит, и мы найдём это место…
Всё тайное становится явным
По умолчанию, PHP хранит сессию в файлах — за это отвечает директива session.save_handler, путь по которому сохраняются файлы ищите в директиве session.save_path, либо воспользуйтесь функцией session_save_path() для получения необходимого пути.
В вашей конфигурации путь к файлам может быть не указан, тогда файлы сессии будут хранится во временных файлах вашей системы — вызовите функцию sys_get_temp_dir() и узнайте где это потаённое место.
Так, идём по данному пути и находим ваш файл сессии (у меня это файл sess_dap83arr6r3b56e0q7t5i0qf91
), откроем его в текстовом редакторе:
time|s:8:"16:19:51";
Как видим — вот оно наше время, вот в каком хитром формате хранится наша сессия, но мы можем внести правки, поменять время, или можем просто вписать любую строку, почему бы и нет:
time|s:13:"m/ (@.@) m/";
Для преобразования этой строки в массив нужно воспользоваться функцией session_decode(), для обратного преобразования — session_encode() — это зовется сериализацией, вот только в PHP для сессий — она своя — особенная, хотя можно использовать и стандартную PHP сериализацию — пропишите в конфигурационной директиве session.serialize_handler значение php_serialize
и будет вам счастье, и $_SESSION
можно будет использовать без ограничений — в качестве индекса теперь вы сможете использовать цифры и специальные символы |
и !
в имени (за все 10+ лет работы, ни разу не надо было :)
session_decode()
, вот вам тестовый набор данных для сессии (для решения знаний регулярных выражений не требуется), текст для преобразования возьмите из файла вашей текущей сессии:
$_SESSION['integer var'] = 123;
$_SESSION['float var'] = 1.23;
$_SESSION['octal var'] = 0x123;
$_SESSION['string var'] = "Hello world";
$_SESSION['array var'] = array('one', 'two', [1,2,3]);
$object = new stdClass();
$object->foo = 'bar';
$object->arr = array('hello', 'world');
$_SESSION['object var'] = $object;
$_SESSION['integer again'] = 42;
Так, что мы ещё не пробовали? Правильно — украсть «печеньки», давайте запустим другой браузер и добавим в него теже самые cookie. Я вам для этого простенький javascript написал, скопируйте его в консоль браузера и запустите, только не забудьте идентификатор сессии поменять на свой:
javascript:(function(){document.cookie='PHPSESSID=dap83arr6r3b56e0q7t5i0qf91;path=/;';window.location.reload();})()
Вот теперь у вас оба браузера смотрят на одну и туже сессию. Я выше упоминал, что расскажу о способах защиты, рассмотрим самый простой способ — привяжем сессию к браузеру, точнее к тому, как браузер представляется серверу — будем запоминать User-Agent и проверять его каждый раз:
session_start();
if (!isset($_SESSION['time'])) {
$_SESSION['ua'] = $_SERVER['HTTP_USER_AGENT'];
$_SESSION['time'] = date("H:i:s");
}
if ($_SESSION['ua'] != $_SERVER['HTTP_USER_AGENT']) {
die('Wrong browser');
}
echo $_SESSION['time'];
Это подделать сложнее, но всё ещё возможно, добавьте сюда ещё сохранение и проверку $_SERVER['REMOTE_ADDR']
и $_SERVER['HTTP_X_FORWARDED_FOR']
, и это уже более-менее будет похоже на защиту от злоумышленников посягающих на наши «печеньки».
Ключевое слово в предыдущем абзаце похоже, в реальных проектах cookies уже давно «бегают» по HTTPS протоколу, таким образом никто их не сможет украсть без физического доступа к вашему компьютеру или смартфону
Стоит упомянуть директиву session.cookie-httponly, благодаря ей сессионная кука будет недоступна из JavaScript'a. Кроме этого — если заглянуть в мануал функции setcookie(), то можно заметить, что последний параметр так же отвечает за HttpOnly. Помните об этом — эта настройка позволяет достаточно эффективно бороться с XSS атаками в практически всех браузерах.
По шагам
А теперь поясню по шагам алгоритм, как работает сессия в PHP, на примере следующего кода (настройки по умолчанию):
session_start();
$_SESSION['id'] = 42;
- после вызова
session_start()
PHP ищет в cookie идентификатор сессии по имени прописанном вsession.name
— этоPHPSESSID
- если нет идентификатора — то он создаётся (см. session_id()), и создаёт пустой файл сессии по пути
session.save_path
с именемsess_{session_id()}
, в ответ сервера будет добавлены заголовки, для установки cookie{session_name()}={session_id()}
- если идентификатор присутствует, то ищем файл сессии в папке
session.save_path
:- не находим — создаём пустой файл с именем
sess_{$_COOKIE[session_name()]}
(идентификатор может содержать лишь символы из диапазоновa-z
,A-Z
,0-9
, запятую и знак минус) - находим, читаем файл и распаковываем данные (см. session_decode()) в супер-глобальную переменную
$_SESSION
- не находим — создаём пустой файл с именем
- когда скрипт закончил свою работу, то все данные из
$_SESSION
запаковывают с использованиемsession_encode()
в файл по путиsession.save_path
с именемsess_{session_id()}
PHPSESSID
, пусть это будет 1234567890
, обновите страницу, проверьте, что у вас создался новый файл sess_1234567890
А есть ли жизнь без «печенек»?
PHP может работать с сессией даже если cookie в браузере отключены, но тогда все URL на сайте будут содержать параметр с идентификатором вашей сессии, и да — это ещё настроить надо, но оно вам надо? Мне не приходилось это использовать, но если очень хочется — я просто скажу где копать:
А если надо сессию в базе данных хранить?
Для хранения сессии в БД потребуется изменить хранилище сессии и указать PHP как им пользоваться, для этой цели создан интерфейс SessionHandlerInterface и функция session_set_save_handler.
Отдельно замечу, что не надо писать собственные обработчики сессий для redis и memcache — когда вы устанавливаете данные расширения, то вместе с ними идут и соответствующие обработчики, так что RTFM наше всё. Ну и да, обработчик нужно указывать до вызова
session_start()
;)
SessionHandlerInterface
для хранения сессии в MySQL, проверьте, работает ли он.Это задание со звёздочкой, для тех кто уже познакомился с базами данных.
Когда умирает сессия?
Интересный вопрос, можете задать его матёрым разработчикам — когда PHP удаляет файлы просроченных сессий? Ответ есть в официальном руководстве, но не в явном виде — так что запоминайте:
Сборщик мусора (garbage collection) может запускаться при вызове функции session_start()
, вероятность запуска зависит от двух директив session.gc_probability и session.gc_divisor, первая выступает в качестве делимого, вторая — делителя, и по умолчанию эти значения 1 и 100, т.е. вероятность того, что сборщик будет запущен и файлы сессий будут удалены — примерно 1%.
session.gc_divisor
так, чтобы сборщик мусора запускался каждый раз, проверьте что это так и происходит.
Самая тривиальная ошибка
Ошибка у которой более полумиллиона результатов в выдаче Google:
Cannot send session cookie — headers already sent by
Cannot send session cache limiter — headers already sent
Для получения таковой, создайте файл session.error.php со следующим содержимым:
echo str_pad(' ', ini_get('output_buffering'));
session_start();
Во второй строке странная «магия» — это фокус с буфером вывода, я ещё расскажу о нём в одной из следующих статей, пока считайте это лишь строкой длинной в 4096 символов, в данном случае — это всё пробелы
Запустите, предварительно удалив cookie, и получите приведенные ошибки, хоть текст ошибок и разный, но суть одна — поезд ушёл — сервер уже отправил браузеру содержимое страницы, и отправлять заголовки уже поздно, это не сработает, и в куках не появилось заветного идентификатора сессии. Если вы стокнулись с данной ошибкой — ищите место, где выводится текст раньше времени, это может быть пробел до символов <?php
, или после ?>
в одном из подключаемых файлов, и ладно если это пробел, может быть и какой-нить непечатный символ вроде BOM, так что будьте внимательны, и вас сия зараза не коснется (как-же,… гомерический смех).
require_once 'include/sess.php';
sess_start();
if (isset($_SESS["id"])) {
echo $_SESS["id"];
} else {
$_SESS["id"] = 42;
}
Для осуществления задуманного вам потребуется функция register_shutdown_function()
В заключение
В этой статье вам дано шесть заданий, при этом они касаются не только работы с сессиями, но так же познакомят вас с MySQL и с функциями работы со строками. Для усвоения этого материала — отдельной статьи не нужно, хватит и мануала по приведенным ссылкам — никто за вас его читать не будет. Дерзайте!
P.S. Если узнали что-то новое из статьи — отблагодарите автора — зашарьте статью в социалках ;)
P.P.S. Да, это кросс-пост статьи с моего блога, но она актуальна и поныне :)
Автор: Антон Шевчук