Сессии - всегда ли они нужны?

Хочу еще раз поднять тему использования сессий для аутентификации пользователей. Надеюсь услышать критику приведенного в статье метода с высоты вашего опыта.

Несмотря на всю продуманность и блестящую реализацию сессий в PHP, большинство разработчиков рано или поздно сталкваются с необходимостью расширения/изменения стандартного функционала. Вот основные моменты, которые приходится решать:
  1. Session Fixation. Чтобы уберечься от воровства сессий, sessid привязывают к какой-то информации, характеризующей пользователя. Обычно это IP или UserAgent, или все вместе.
  2. Свой session_set_save_handler. Поскольку хранить переменные сессии в фаловой системе - решение далеко не оптимальное, рано или поздно приходится задумываться о переносе сессий в memcached, базу, или еще куда.
  3. session.use_trans_sid = 0. Встроенный в PHP механизм url_rewriter - необычайно мощная и полезная штука, но при использовании ее в сессиях возникает серия неприятных моментов. "Взбесившиеся" поисковики, некрасивые ссылки, заход под чужим именем, "засвет" своего sessid в логах совершенно посторонних сайтов (через HTTP_REFERER) и т.д. Посему обычно эту функцию отключают, и для передачи sessid используют исключительно куки.
На последнем пункте я хочу остановиться подробнее. Дело в том, что большинство крупных сайтов (google, ozon, livejournal, paypal..) работают только через куки. Мне это показалось очень странным, поскольку непохоже, чтобы люди без кук в интернете встречаются аж настолько редко, чтобы ими можно было пренебречь.

Статистика liveinternet показывает, что число людей с отключенными куками держится в районе 4%. Не так уж и мало. Вряд ли такие гиганты сознательно отсекали бы эту часть аудитории, по крайней мере на то должны были быть очень веские основания. И они нашлись.

Оказывается, эти 4% включают тех людей, у которых не работают постоянные куки (с жестко заданным lifetime), но работают сессионные - с lifetime=0. А сессионные куки работают практически у всех, даже у тех, кто в настройках безопасности поставил "запретить куки", и даже в lynx. :)

Понятно, что 100% гарантии все равно дать нельзя. Например, куки может резать фаервол (хотя опять не известно, будет ли он резать с lifetime=0). У пользователя может быть самописный броузер без поддержки кук (например, crawler на перле). Да мало ли чего еще..

Но судя по своему опыту, (а опыт "старших товарищей" это косвенно подтверждает), скажу, что на практике можно смело рассчитывать, что сессионные куки у пользователя таки да, есть. И также в пользу этого тезиса говорит и то, что по умолчанию в PHP параметр session.use_only_cookies установлен в 1, т.е. у людей без кук сессии не работают.

А раз мы можем рассчитывать на поддержку кук, то в большинстве мест, где обычно используются сессии, на практике мы можем через куки все сделать проще и удобнее, и при этом работать это будет во всех тех же случаях, что и сессии. Почему я говорю проще? Потому что куку кинул - и забыл, ее не надо прописывать вручную при header(Location:..), о ней не надо помнить при работе с аяксом, она присутствует в запросе не зависимо от того, относится ли этот запрос к скрипту, статической html, картинке или css. О сессиях же забыть получится только в случае, если они работают через те же куки, да и то не всегда. :)

Теперь пара слов о собственном session_set_save_handler. Конечно, тут все зависит от того, какие данные и как долго нам нужно хранить. Для общего случая, конечно, нужно использовать базу или файловую систему. Если время жизни сессии невелико, а сами данные в сессии легко восстановимы, то вполне сгодится и память (или memcached). А если сессия используется только для аутентификации пользователя, то стоит задуматься, нужно ли вообще в принципе хранить что-то на стороне сервера. Ведь при использовании кук мы можем полностью отказаться от save_handler, и хранить все данные у клиента.

Опять же, беглое изучение кук, оставляемых phpbb, wordpress, gmail и др. показало, что такой подход вполне часто используется и вполне имеет право на жизнь. Единственное, о чем стоит помнить, это что куки легко могут быть подделаны, а значит слепо доверять им ни в коем случае нельзя.

И тут мы подходим к пункту 1 - Session Fixation. Как и при использовании стандартного механизма сессий, свою куку мы также должны привязывать к какой-то информации, идентифицирующей пользователя, чтобы исключить возможность передачи ее другому. Кроме того, мы должны защитить куку от возможных изменений самим пользователем.

Как и в случае с сессиями, можно это сделать путем сохранения на сервере и последующей сверкой нужной информации (тех же IP и UserAgent). Но мы же решили на сервере ничего не хранить. Посмотрим, можно ли это сделать без использования памяти сервера.

Рассмотрим распространенный случай авторизации: при вводе правильного логина и пароля мы сохраняем у пользователя в куке его уникальный идентификатор (ID), и потом при каждом обращении к серверу опознаем пользователя по этому идентификатору и считаем залогиненым. Если идентификатора нет, или время истекло - мы опять спрашиваем у пользователя логин и пароль.

Какие недостатки тут есть:
  1. Пользователь легко может изменить ID в куке, и будет опознан нами как другой пользователь.
  2. Пользователь может украсть куку у другого пользователя и выдать себя за него.
  3. Не понятно, как определить, что время вышло. Мы на сервере не храним никакого таймаута, посему не знаем, зашел ли к нам пользователь вчера или он уже два месяца приходит с одной и той же кукой. Мы не помним, когда он к нам логинился конкретно с этого компьютера.
Для того, чтобы пользователь не мог изменить данные в куке, мы можем использовать цифровую подпись. Например, md5 от какого-то секретного слова и id пользователя. Или от пароля этого пользователя. Или от хеша пароля, если сам пароль мы в базе не храним. Короче, нам нужна такая информация, которую пользователь знает только о себе, но не знает о других пользователях, за которых он себя хочет выдать. Или же не знает вообще (секретное слово). Таким образом, кука, которую мы ставим, будет иметь вид:
  $cookie  =  $userid . '|' . md5($userid . 'secret word');
Для того, чтобы пользователь не мог прислать куку другого пользователя, мы в той же цифровой подписи используем IP и UserAgent.
  $cookie = $userid . '|' . md5($userid . 'secret word' .   
       $_SERVER['REMOTE_ADDR'] )
При получении куки мы проверяем подпись, используя те UserAgent и IP, с которыми эта кука к нам пришла. Если в подписи куки использовались не те значения, что пришли сейчас - подпись окажется не верна, и куку мы не примем.

И наконец, время действия. Проще всего вообще на это забить: пока юзер присылает нам правильную куку c правильного IP и UserAgent - мы его пускаем. Но если мы все-таки хотим насильно ограничить время действия сессии, мы можем дописать крайний срок в саму куку. И тоже подписать.
  $cookie = $userid . '|' . $time . '|' . md5(
	$userid . $time . 'secret word' . 
	$_SERVER['REMOTE_ADDR'] . $_SERVER['HTTP_USER_AGENT']
  )


Что мы имеем в итоге: вполне надежный механизм аутентификации пользователей на сайте, создающий минимальную нагрузку на сервер.

Чего мы не имеем: мы не можем хранить много сессионных данных. Размер куки ограничен, md5 на длинных строках жрет процессорное время, да и негоже пользователю каждый раз качать туда-сюда весь этот мусор. Максимальную длину, наверное, стоит сделать как у gmail - порядка 120 байт. Хотя чего там можно столько хранить в сессии - я не знаю. В любом случае, если надо хранить много переменных - то имхо стоит все же использовать стандартные PHP sessions, которые разработаны для общего случая и вполне могут использоваться и в нашем, пусть и с меньшей производительностью.

Еще мы не знаем, сколько сессий у нас в данный момент открыто. Их можно открывать неограничено много. В принципе, ничто не мешает нам вести такой учет, но мы же сами хотели разгрузить сервер..

Чем это лучше, чем использовать стандартные сессии, но со своим save_handler и session_fixation? Тем, что тут все происходит на виду и в любое место можно вмешаться. Простота кода. Ну и скорость - в обмен на универсальность.

Автор: http://fogx.habrahabr.ru/



Опубликовал admin
19 Мар, Среда 2008г.



Программирование для чайников.