PHP для начинающих. Сессия // PHP

Всем хорошего дня. Перед вами первая статья из серии 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 ... ...
Это очень упрощенные примеры, но и тут можно увидеть из чего состоят HTTP запрос и ответ:
- стартовая строка – для запроса содержит метод и путь запрашиваемой страницы, для ответа – версию протокола и код ответа
- заголовки – имеют формат ключ-значение разделенные двоеточием, каждый новый заголовок пишется с новой строки
- тело сообщения – непосредственно HTML либо данные отделяют от заголовков двумя переносами строки, могут отсутствовать, как в приведенном запросе
Так, вроде с протоколом разобрались – он простой, ведёт свою историю аж с 1992-го года, так что идеальным его не назовешь, но какой есть – отправили запрос – получите ответ, и всё, сервер и клиент никоим образом более не связаны. Но подобный сценарий отнюдь не единственный возможный, у нас же может быть авторизация, сервер должен каким-то образом понимать, что вот этот запрос пришёл от определенного пользователя, т.е. клиент и сервер должны общаться в рамках некой сессии. И да, для этого придумали следующий механизм:
- при авторизации пользователя, сервер генерирует и запоминает уникальный ключ – идентификатор сессии, и сообщает его браузеру
- браузер сохраняет этот ключ, и при каждом последующем запросе, его отправляет
Для реализации этого механизма и были созданы cookie (куки, печеньки) – простые текстовые файлы на вашем компьютере, по файлу для каждого домена (хотя некоторые браузеры более продвинутые, и используют для хранения SQLite базу данных), при этом браузер накладывает ограничение на количество записей и размер хранимых данных (для большинства браузеров это 4096 байт, см. RFC 2109 от 1997-го года)
Т.е. если украсть cookie из вашего браузера, то можно будет зайти на вашу страничку в facebook от вашего имени? Не пугайтесь, так сделать нельзя, по крайней мере с facebook, и дальше я вас научу как можно защититься от данного вида атаки на ваших пользователей.
Давайте теперь посмотрим как изменятся наши запрос-ответ, будь там авторизация:
POST /login/ HTTP/1.1 Host: example.com Accept: text/html login=Username&password=Userpass
Метод у нас изменился на POST, и в теле запроса у нас передаются логин и пароль (если использовать метод GET, то строка запроса будет содержать логин и пароль, и может оказаться сохраненной на каких нибудь промежуточных прокси серверах, что очень плохо).
HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 Set-Cookie: KEY=VerySecretUniqueKey ... ...
Ответ сервер будет содержать заголовок Set-Cookie: KEY=VerySecretUniqueKey
, что заставит браузер сохранить эти данные в файлы cookie, и при следующем обращении к серверу – они будут отправлены и опознаны сервером:
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 со следующим содержимым:
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'] = ['one', 'two', [1,2,3]]; $object = new stdClass(); $object->foo = 'bar'; $object->arr = ['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 атаками в практически всех браузерах.
Задание
Добавьте в код проверку на IP пользователя, если проверка не прошла – удалите скомпрометированную сессию.
По шагам
А теперь поясню по шагам алгоритм, как работает сессия в 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. Если узнали что-то новое из статьи – отблагодарите автора – зашарьте статью в социалках ;)