# Сессия

Сессии — это один из самых важных компонентов, с которыми вам придется работать. Не понимая принципов его работы - наворотите делов. Так что во избежание проблем я постараюсь рассказать о всех возможных нюансах.

Но для начала, чтобы понять зачем нам сессия, обратимся к истокам - к HTTP протоколу.

?? Язык PHP создавался под стать этому протоколу — т.е. основная его задача — это дать ответ на HTTP запрос и "умереть" освободив память и ресурсы. ?? Под HTTP протокол подстроился и механизм сессий, т.о. приходится расчитывать только на себя, ведь сервер при повторном запросе будет забывать обо всём происходящем ранее. ?? И уже как следствие данных , одна из тривиальных задач стоящих перед web-разработчиком это как раз следить за этой самой сессией.

> *Вот вам статейка на тему* [*PHP is meant to die*](http://software-gunslinger.tumblr.com/post/47131406821/php-is-meant-to-die)*, или вот она же* [*на русском языке*](https://habrahabr.ru/post/179399/)*, но лучше отложите её в закладки "на потом".*

Перво-наперво необходимо "стартовать" сессию - для этого воспользуемся функцией [session\_start()](http://php.net/function.session-start), создайте файл *session.start.php* со следующим содержимым:

```php
<?php
session_start();
```

Запустите встроенный в PHP [web-server](http://php.net/features.commandline.webserver) в папке с вашим скриптом:

```bash
php -S 127.0.0.1:8080
```

Запустите браузер, и откройте в нём Developer Tools или [что там у вас](https://en.wikipedia.org/wiki/Web_development_tools#Web_developer_tools_support), далее перейдите на страницу <http://127.0.0.1:8080/session.start.php> — вы должны увидеть лишь пустую страницу, но не спешите закрывать — посмотрите на заголовки которые нам прислал сервер:

![Cookie](https://3289752752-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-M50cHX4iDeDG7TarMwE%2F-M50cHxlbFtIHH4R9kDw%2F-M50cOcByFmCoDnQLY1f%2Fsession.start.cookie.png?generation=1587017801441123\&alt=media)

Там будет много чего, интересует нас только вот эта строчка в ответе сервера (почистите куки, если нет такой строчки, и обновите страницу):

```
Set-Cookie: PHPSESSID=dap83arr6r3b56e0q7t5i0qf91; path=/
```

Увидев сие, браузер сохранит у себя куку с именем `PHPSESSID`:

![Browser session cookie](https://3289752752-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-M50cHX4iDeDG7TarMwE%2F-M50cHxlbFtIHH4R9kDw%2F-M50cOcDK37HI8JSdbDS%2Fsession.start.browser.png?generation=1587017800362005\&alt=media)

> *`PHPSESSID` - имя сессии по умолчанию, регулируется из конфига php.ini директивой* [*session.name*](http://php.net/session.configuration#ini.session.name)*, при необходимости имя можно изменить в самом конфигурационном файле или с помощью функции* [*session\_name()*](http://php.net/function.session-name)

И теперь - обновляем страничку, и видим, что браузер отправляет эту куку на сервер, можете попробовать пару раз обновить страницу, результат будет идентичным:

![Browser request with cookie](https://3289752752-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-M50cHX4iDeDG7TarMwE%2F-M50cHxlbFtIHH4R9kDw%2F-M50cOcF7dk8XdGCNpKJ%2Fsession.start.request.png?generation=1587017799474438\&alt=media)

Итого, что мы имеем - теория совпала с практикой, и это просто отлично.

Следующий шаг - сохраним в сессию произвольное значение, для этого в PHP используется супер-глобальная переменная `$_SESSION`, сохранять будем текущее время - для этого вызовем функцию [date()](http://php.net/function.date):

```php
session_start();
$_SESSION['time'] = date("H:i:s");
echo $_SESSION['time'];
```

Обновляем страничку и видим время сервера, обновляем ещё раз - и время обновилось. Давайте теперь сделаем так, чтобы установленное время не изменялось при каждом обновлении страницы:

```php
session_start();
if (!isset($_SESSION['time'])) {
    $_SESSION['time'] = date("H:i:s");
}
echo $_SESSION['time'];
```

Обновляем - время не меняется, то что нужно. Но при этом мы помним, PHP умирает, значит данную сессию он где-то хранит, и мы найдём это место...

## Всё тайное становится явным

По умолчанию, PHP хранит сессию в файлах - за это отвечает директива [session.save\_handler](http://php.net/session.configuration#ini.session.save-handler), путь по которому сохраняются файлы ищите в директиве [session.save\_path](http://php.net/session.configuration#ini.session.save-path), либо воспользуйтесь функцией [session\_save\_path()](http://php.net/function.session-save-path) для получения необходимого пути.

> *В вашей конфигурации путь к файлам может быть не указан, тогда файлы сессии будут хранится во временных файлах вашей системы - вызовите функцию* [*sys\_get\_temp\_dir()*](http://php.net/sys_get_temp_dir) *и узнайте где это потаённое место.*

Так, идём по данному пути и находим ваш файл сессии (у меня это файл `sess_dap83arr6r3b56e0q7t5i0qf91`), откроем его в текстовом редакторе:

```
time|s:8:"16:19:51";
```

Как видим - вот оно наше время, вот в каком хитром формате хранится наша сессия, но мы можем внести правки, поменять время, или можем просто вписать любую строку, почему бы и нет:

```
time|s:13:"\m/ (@.@) \m/";
```

Для преобразования этой строки в массив нужно воспользоваться функцией [session\_decode()](http://php.net/function.session-decode), для обратного преобразования - [session\_encode()](http://php.net/function.session-encode) - это зовется сериализацией, вот только в PHP для сессий - она своя - особенная, хотя можно использовать и стандартную [PHP сериализацию](http://php.net/function.serialize) - пропишите в конфигурационной директиве [session.serialize\_handler](http://php.net/session.configuration#ini.session.serialize-handler) значение `php_serialize` и будет вам счастье, и `$_SESSION` можно будет использовать без ограничений - в качестве индекса теперь вы сможете использовать цифры и специальные символы `|` и `!` в имени (за все 10+ лет работы, ни разу не надо было :)

%accordion%Задание%accordion%

Напишите свою функцию, аналогичную по функционалу `session_decode()`, вот вам тестовый набор данных для сессии (для решения знаний регулярных выражений не требуется), текст для преобразования возьмите из файла вашей текущей сессии:

```php
$_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-&gt;foo = 'bar';
$object-&gt;arr = array('hello', 'world');

$_SESSION['object var'] = $object;
$_SESSION['integer again'] = 42;
```

%/accordion%

Так, что мы ещё не пробовали? Правильно - украсть "печеньки", давайте запустим другой браузер и добавим в него теже самые cookie. Я вам для этого простенький javascript написал, скопируйте его в консоль браузера и запустите, только не забудьте идентификатор сессии поменять на свой:

```javascript
javascript:(function(){document.cookie='PHPSESSID=dap83arr6r3b56e0q7t5i0qf91;path=/;';window.location.reload();})()
```

Вот теперь у вас оба браузера смотрят на одну и туже сессию. Я выше упоминал, что расскажу о способах защиты, рассмотрим самый простой способ - привяжем сессию к браузеру, точнее к тому, как браузер представляется серверу — будем запоминать [User-Agent](https://en.wikipedia.org/wiki/User_agent) и проверять его каждый раз:

```php
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](http://php.net/session.configuration.php#ini.session.cookie-httponly), благодаря ей сессионная кука будет недоступна из JavaScript'a. Кроме этого - если заглянуть в мануал функции [setcookie()](http://php.net/function.setcookie), то можно заметить, что последний параметр так же отвечает за HttpOnly. Помните об этом - эта настройка позволяет достаточно эффективно бороться с XSS атаками в практически [всех браузерах](http://www.browserscope.org/?category=security).

%accordion%Задание%accordion% Добавьте в код проверку на IP пользователя, если проверка не прошла - удалите скомпрометированную сессию. %/accordion%

## По шагам

А теперь поясню по шагам алгоритм, как работает сессия в PHP, на примере следующего кода (настройки по умолчанию):

```php
session_start();
$_SESSION['id'] = 42;
```

1. после вызова \`session\_start()\` PHP ищет в cookie идентификатор сессии по имени прописанном в \`session.name\` - это \`PHPSESSID\`
2. если нет идентификатора - то он создаётся (см. [session\_id()](http://php.net/function.session-id)), и создаёт пустой файл сессии по пути \`session.save\_path\` с именем \`sess\_{session\_id()}\`, в ответ сервера будет добавлены заголовки, для установки cookie \`{session\_name()}={session\_id()}\`
3. если идентификатор присутствует, то ищем файл сессии в папке \`session.save\_path\`:
   * не находим - создаём пустой файл с именем \`sess\_{$\_COOKIE\[session\_name()]}\` (идентификатор может содержать лишь символы из диапазонов \`a-z\`, \`A-Z\`, \`0-9\`, запятую и знак минус)
   * находим, читаем файл и распаковываем данные (см. [session\_decode()](http://php.net/function.session-decode)) в супер-глобальную переменную \`$\_SESSION\` (файл блокируется для чтения/записи)
4. когда скрипт закончил свою работу, то все данные из \`$\_SESSION\` запаковывают с использованием \`session\_encode()\` в файл по пути \`session.save\_path\` с именем \`sess\_{session\_id()}\` (блокировка снимается)

%accordion%Задание%accordion% Задайте в вашем браузере произвольное значение куки с именем `PHPSESSID`, пусть это будет `1234567890`, обновите страницу, проверьте, что у вас создался новый файл `sess_1234567890` %/accordion%

## А есть ли жизнь без "печенек"?

PHP может работать с сессией даже если cookie в браузере отключены, но тогда все URL на сайте будут содержать параметр с идентификатором вашей сессии, и да - это ещё настроить надо, но оно вам надо? Мне не приходилось это использовать, но если очень хочется - я просто скажу где копать:

* [session.use\_cookies](http://php.net/session.configuration#ini.session.use-cookies)
* [session.use\_only\_cookies](http://php.net/session.configuration#ini.session.use-only-cookies)

## А если надо сессию в базе данных хранить?

Для хранения сессии в БД потребуется изменить хранилище сессии и указать PHP как им пользоваться, для этой цели создан интерфейс [SessionHandlerInterface](http://php.net/class.sessionhandlerinterface) и функция [session\_set\_save\_handler](http://php.net/function.session-set-save-handler).

> *Отдельно замечу, что не надо писать собственные обработчики сессий для redis и memcache - когда вы устанавливаете данные расширения, то вместе с ними идут и соответствующие обработчики, так что RTFM наше всё. Ну и да, обработчик нужно указывать до вызова `session_start()` ;)*

%accordion%Задание%accordion% Реализуйте `SessionHandlerInterface` для хранения сессии в MySQL, проверьте, работает ли он. Это задание со звёздочкой, для тех кто уже познакомился с базами данных. %/accordion%

## Когда умирает сессия?

За время жизни сессии отвечает директива [session.gc\_maxlifetime](http://php.net/manual/session.configuration.php#ini.session.gc-maxlifetime). По умолчанию, данная директива равна 1440 секундам (24 минуты), понимать её следует так, что если к сессии не было обращении в течении заданного времени, то сессия будет считаться «протухшей» и будет ждать своей очереди на удаление.

Интересен другой вопрос, можете задать его матёрым разработчикам - когда PHP удаляет файлы просроченных сессий? Ответ есть в официальном руководстве, но не в явном виде - так что запоминайте:

Сборщик мусора (garbage collection) может запускаться при вызове функции `session_start()`, вероятность запуска зависит от двух директив [session.gc\_probability](http://php.net/session.configuration#ini.session.gc-probability) и [session.gc\_divisor](http://php.net/session.configuration#ini.session.gc-divisor), первая выступает в качестве делимого, вторая - делителя, и по умолчанию эти значения 1 и 100, т.е. вероятность того, что сборщик будет запущен и файлы сессий будут удалены - примерно 1%.

%accordion%Задание%accordion% Измените значение директивы `session.gc_divisor` так, чтобы сборщик мусора запускался каждый раз, проверьте что это так и происходит. %/accordion%

## Самая тривиальная ошибка

Ошибка у которой более полумиллиона результатов в выдаче Google:

> Cannot send session cookie - **headers already sent** by Cannot send session cache limiter - **headers already sent**

Для получения таковой, создайте файл *session.error.php* со следующим содержимым:

```php
echo str_pad(' ', ini_get('output_buffering'));
session_start();
```

> *Во второй строке странная «магия» - это фокус с буфером вывода, я уже рассказывал о нём в* [*одноимённой главе*](https://antonshevchuk.gitbook.io/php-for-beginners/10_basic/output-buffer)*. Да-да, мы забиваем этот буфер пробелами до длинны в 4096 символов*

Запустите, предварительно удалив cookie, и получите приведенные ошибки, хоть текст ошибок и разный, но суть одна — поезд ушёл — сервер уже отправил браузеру содержимое страницы, и отправлять заголовки уже поздно, это не сработает, и в куках не появилось заветного идентификатора сессии. Если вы стокнулись с данной ошибкой - ищите место, где выводится текст раньше времени, это может быть пробел до символов `&lt;?php`, или после `?&gt;` в одном из подключаемых файлов, и ладно если это пробел, может быть и какой-нить непечатный символ вроде [BOM](https://en.wikipedia.org/wiki/Byte_order_mark), так что будьте внимательны, и вас сия зараза не коснется (как-же, … гомерический смех).

%accordion%Задание%accordion% Для проверки полученных знаний, я хочу, чтобы вы реализовали свой собственный механизм сессий и заставили приведенный код работать:

```php
require_once 'include/sess.php';
sess_start();
if (isset($_SESS["id"])) {
    echo $_SESS["id"];
} else {
    $_SESS["id"] = 42;
}
```

> *Для осуществления задуманного вам потребуется функция* [*register\_shutdown\_function()*](http://php.net/function.register-shutdown-function)

%/accordion%

## Блокировка

Ещё одна распространённая ошибка у новичков — это попытка прочитать файл сессии пока он заблокирован другим скриптом. Собственно, это не совсем ошибка, это недопонимание принципа блокировки :)

Но давайте ещё раз по шагам:

1. \`session\_start()\` не только создаёт/читает файл, но и блокирует его, чтобы никто не мог внести правки в момент выполнения скрипта, или прочитать не консистентные данные из файла сессии
2. блокировка снимается по окончанию выполнения скрипта

«Воткнутся» в данную ошибку очень легко, создайте два файла:

```php
// файл start.php
session_start();
echo "OK";
```

```php
// файл lock.php
session_start();
sleep(10);
echo "OK";
```

Теперь, если вы откроете в браузере страничку `lock.php`, а затем в новой вкладке откроете `start.php` то увидите, что вторая страничка откроется только после того, как отработает первый скрипт, который блокирует файл сессии на 10 секунд.

Есть пару вариантов, как избежать подобного явления — «топорный» и «продуманный».

**«Топорный»** Использовать самописный обработчик сессий, в котором «забыть» реализовать блокировку :) Чуть лучше вариант, это взять готовый и отключить блокировку (например у memcached есть такая опция — [memcached.sess\_locking](http://php.net/manual/memcached.configuration.php#ini.memcached.sess-locking)) O\_o Потратить часы на дебаг кода в поисках редко всплывающей ошибки...

**«Продуманный»** Куда как лучше — самому следить за блокировкой сессии, и снимать её, когда она не требуется:

— Если вы уверенны, что вам не потребуется вносить изменения в сессионные данные используйте опцию `read_and_close` при старте сессии:

```php
session_start([
    'read_and_close' => true
]);
```

Таким образом, блокировка будет снята сразу по прочтению данных сессии.

— Если вам таки нужно вносить изменения в сессию, то после внесения оных закрывайте сессию от записи:

```php
session_start();
// some changes
session_write_close();
```

%accordion%Задание%accordion% Чуть выше был приведён листинг двух файлов `start.php` и `lock.php`, создайте ещё файлы `read-close.php` и `write-close.php`, в которых вы будете контролировать блокировку перечисленными способами. Проверьте как работает (или не работает) блокировка. %/accordion%

## В заключение

В этой статье вам дано семь заданий, при этом они касаются не только работы с [сессиями](http://php.net/ref.session), но так же познакомят вас с [MySQL](http://php.net/book.mysqli) и с [функциями работы со строками](http://php.net/ref.strings). Для усвоения этого материала вам хватит мануала по приведенным ссылкам — никто за вас его читать не будет. Дерзайте!
