Хочу ще раз підняти тему використання сесій для автентифікації користувачів. Сподіваюся почути критику наведеного в статті методу з висоти вашого досвіду.
Незважаючи на всю продуманість і блискучу реалізацію сесій у PHP, більшість розробників рано чи пізно сталквуються з необхідністю розширення/зміни стандартного функціоналу. Ось основні моменти, які доводиться вирішувати:
- Session Fixation. Щоб уберегтися від злодійства сесій, sessid прив'язують до якоїсь інформації, що характеризує
користувача. Зазвичай це IP або UserAgent, або всі разом.
Свій session_set_save_handler. Оскільки зберігати змінні сесії у фаловій системі - рішення далеко не оптимальне,
рано чи пізно доводиться замислюватися про перенесення сесій в memcached, базу, або ще куди.
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), і потім при кожному зверненні до сервера розпізнаємо користувача за цим ідентифікатором
і вважаємо заставленим. Якщо ідентифікатора немає, або час минув - ми знову запитуємо у користувача логін і пароль.
Які недоліки тут є:
- Користувач легко може змінити ІД в куці, і буде упізнаний нами як інший користувач.
Користувач може вкрасти куку в іншого користувача і видати себе за нього.
Не зрозуміло, як визначити, що час вийшов. Ми на сервері не зберігаємо ніякого таймауту, тому не знаємо, чи зайшов
до нас користувач вчора або він вже два місяці приходить з однією і тією ж кукою. Ми не пам'ятаємо, коли він до нас логінився
конкретно з цього комп'ютера.
Для того, щоб користувач не міг змінити дані в куці, ми можемо використовувати цифровий підпис. Наприклад, 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? Тим, що тут все відбувається на виду і в будь-яке місце можна втрутитися. Простота коду. Ну і швидкість - в обмін на універсальність.





