Давно не писал про Kohana 2.3.x. Увлекся третьей версией, забросил (надеюсь, временно) написание форума как части CMS, в общем стыдно стало. Решил разобрать по косточкам наверное самый популярный модуль из дистрибутива, речь пойдет конечно же о модуле Auth.
Что внутри?
Загляните в директорию MODPATH/auth:
auth |---config | |-----auth.php | |---libraries | |-----drivers | | |-----Auth | | | |---file.php | | | |---orm.php | | | | | |-----auth.php | | | |-----auth.php | |---models |-----auth_role.php |-----auth_user.php |-----auth_user_token.php |-----role.php |-----user.php |-----user_token.php
Конфигурация
Конфигурационный файл config/auth.php содержит немногочисленные настройки модуля:
- $config['driver'] — используемый тип драйвера. В комплекте есть ORM и файлы (file). О них чуть позже.
- $config['hash'] — алгоритм для получения хешей. Нужен для безопасного хранения паролей в хранилище (БД или файлы). По умолчанию sha1, может быть использован любой из списка методов, полученных в результате работы функции hash_algos().
- $config['salt_pattern'] — т.н. «соль» хеша. По сути это номера смещений в строке с паролем, куда будут вставлены дополнительные символы, усложняющие взлом хешированного пароля.
- $config['lifetime'] — время жизни cookie в секундах. Если Вы предоставляете возможность логина с «запоминанием» (т.е. чтобы в следующий раз не вводить заново логин и пароль), то данный параметр определяет время, в течении которого в браузере сохранятся данные. По умолчанию 1209600 секунд (две недели).
- $config['session_key'] — имя ключа для хранения текущего пользователя в сессии. По умолчанию ‘session_key‘ (т.е. будет доступен как $_SESSION['session_key']).
- $config['users'] — массив с первоначально доступными пользователями, если планируется использование драйвера file. По умолчанию пустой, но есть закомментированная строка с пользователем ‘admin‘ и паролем ‘admin‘ (в конфиге хранится не сам пароль, а его хеш).
Класс Auth, как уже было сказано ранее, позволяет работать с различными способами хранения и обработки данных, для этого используются драйверы. Однако имеющийся драйвер на основе файлов имеет очень ограниченные возможности (практически только login/logout), данные о пользователях состоят только их логина и пароля, хранятся в текстовых файлах (точнее в упомянутом ранее $config['users']) и подгружаются все сразу. Поэтому обычно они в документации не фигурируют. А вот ORM-драйвер намного более продуктивен, так что в дальнейшем будем рассматривать только его использование.
Рассмотрим основные возможности библиотеки Auth на примере типовых действий пользователя. Поскольку обычно работа с пользователями проводится во всех контроллерах проекта, удобно в базовом контроллере сохранять экземпляр объекта Auth, чтобы в дальнейшем использовать его как свойство (property) контроллера, например так:
$this->auth = Auth::instance(); |
Предисловие. Хеши, соли и прочие пряности
Заходу на сайт конечно же должна предшествовать регистрация. А после регистрации пользователя необходимо сохранить пароль, но не в явном же виде! Поэтому используются различные алгоритмы хеширования. В Kohana используется шифрование с применением «соли«, что увеличивает сложность расшифровки хранимого пароля. Давайте рассмотрим такой код:
// в файле config/auth.php $config['salt_pattern'] = '1, 3, 5, 9, 14, 15, 20, 21, 28, 30'; // в тестовом методе $password = '123456789abcdefg'; echo $this->auth->hash($password)."<br />"; echo $this->auth->hash_password($password); |
Данный пример в результате выведет что-то вроде
'e8dabc6b7e1fb46b08d591c66dde7fb783a1dbe4'
'c66692385b1c5aaefef96fc9d94f4a56ee72f63bd8375a4a07'
Если вы несколько раз обновите страницу, то станет заметно, что первая строка не изменяется, а вторая каждый раз разная. Дело в том, что в первой выводится результат простого хеширования введенного пароля (метод hash($password)), он больше ни от чего не зависит. Во втором же случае мы применяем хеширование с «солью«, в результате чего хеш становится совершенно другим.
Итак, это делается в методе hash_password($password, $salt = FALSE). Первый параметр ясен — это пароль в первоначальном виде (нешифрованный). Второй параметр — строка с «солеными» символами. После регистрации данный метод вызывается без параметра $salt, и соль генерируется автоматически (берется хеш от случайного GUID и обрезается до количества элементов в параметре $config['salt_pattern']). Далее самое интересное — соль вставляется в строку с паролем в качестве префикса (т.е. получается $salt.$password) и все это отправляется в метод hash(). Полученный в результате хеш дополнительно разбавляется символами «соли» в местах, указанных параметром $config['salt_pattern'] (например, если в паттерне первым элементом идет единица, то после первого символа пароля будет вставлен один символ «соли«).
Понятно? Если да, то вы наверное ошиблись сайтом. Проще всего рассмотреть пример, показывающий этапы вычисления хеша:
// пароль $password = '123456789abcdefg'; // соль $salt = '0000000000'; // соль после хеширования, урезанная до нужной длины $hashed_salt = substr($this->auth->hash($salt), 0, strlen($salt)); // простой хэш пароля echo $this->auth->hash($password)."<br />"; // хэш соли echo $hashed_salt."<br />"; // хэш строки из соли и пароля echo $this->auth->hash($hashed_salt.$password)."<br />"; // хэш строки из соли и пароля с "вкраплениями соли" echo $this->auth->hash_password($password, $hashed_salt); |
На выходе получим такие строки:
e8dabc6b7e1fb46b08d591c66dde7fb783a1dbe4
8104ba1dc0
0171bfa8e8a0450af6972cc61c6c20407a65bf47
081711b0fa8e48a045b0aaf69712dcc61c6cc200407a65bf47
На первую можете особо не обращать внимание, она просто показывает, насколько далек обычный хеш пароля от полученного с применением «соли«. Вторая строка — это используемый хеш «соли«, из него берутся символы для вставки в хеш пароля. Например, первый символ (цифра восемь) будет вставлен после первого символа пароля (так указано в salt_pattern). Третья строка — это практически итоговый хеш, но до вставки элементов «соли«. Он отличается от хеша просто пароля, т.к. в начало строки была вставлена переменная $hashed_salt. Ну, а между третьей и четвертой строкой даже видно сходство. Итоговый хеш «разбух» еще на 10 символов (столько элементов в salt_pattern), места вставки «крупиц соли» вы легко увидите. Надеюсь, что теперь стало понятнее.
Надеюсь, вы понимаете, что с разной «солью«. получатся разные хеши. Более того, важна не только сама строка с «солью«, но и параметр $config['salt_pattern'].
Еще одно предисловие. Роли
Драйвер ORM предусматривает использование ролей. У каждого пользователя может быть свой набор ролей, определяющий доступные или запрещенные действия. Например, для удачной авторизации помимо совпадения паролей необходимо наличие роли «login«. Это очень удобно, когда надо лишить пользователя возможности войти на сайт, но и удалять неохота (этакий бан). Для входа в «админку» обычно добавляется роль «admin» и т.д. Можно создавать роли и для исключения каких-то действий (например, на запрет написания статей/комментариев или просмотра фотогалерей).
Роли и пользователи между собой взаимодействуют с помощью ORM-моделей Auth_User и Auth_Role. Так как у каждого пользователя может быть несколько ролей, и роль может быть назначена разным пользователям, связь между ними «много-ко-многим» (has_and_belongs_to_many). Напомню, что для работы с таким видом связей в ORM используются методы has(), add() и remove().
Роли в Auth поддерживают метод unique_id($id), поэтому можно использовать конструкции вида ORM::factory(‘role’, ‘login’);
Вход
Пользователь вводит свои логин и пароль, которые передаются формой для обработки контроллером. Для проверки корректности введенных данных существует метод login($username, $password, $remember = FALSE). Первые два параметра понятны, третий — та самая «галочка» для автоматического входа в дальнейшем. В нашем контроллере проверка будет происходить примерно так:
if ($post = $this->input->post()) { // в $_POST есть данные, значит кто-то пытается войти if ( ! $this->auth->login($post['username'], $post['password'], $post['rememberme'])) { // логин не удался, лучше всего перезагрузить страницу url::redirect(Router::$current_uri); } else { // вошли на сайт! Надо перенаправить пользователя url::redirect(); } // иначе просто показываем форму для входа на сайт } |
Метод login() возвращает булевский результат TRUE/FALSE, в зависимости от которого мы либо можем смело обрабатывать пользователя как залогиненного (как правило это редикт обратно на страницу, откуда был совершен переход к форме входе), либо доложить об ошибке и предложить попробовать еще (опять же, делаем редирект на текущий URL, чтобы очистить массив $_POST). В первой строчке проверяется наличие POST-данных вообще, так как удобно совместить в данном методе как просто вывод формы входа, так и авторизацию.
А что происходит внутри метода login()? Давайте заглянем.
Пустые пароли не поддерживаются вообще (метод сразу возвращает FALSE).
Далее необходимо сравнить пароли. Так как в БД пароль хранится в захешированном виде, надо сравнивать хеши паролей. Как уже было сказано выше, если не знать «соль«, использованную при первом хешировании, второй хеш того же пароля получится совершенно другим. Поэтому для извлечения «соли» из готового хеша используется метод find_salt($password). Он использует указанный в конфигурации шаблон (salt_pattern) и выдергивает из хранящегося хэша символы.
Теперь наверное станет понятно, почему смена salt_pattern приводит к потере аккаунтов — невозможно получить правильную «соль» из старых паролей. Это очень распространенная ошибка, даже занес ее в свой FAQ…
Далее все просто — сравниваем полученный хеш со старым и выдаем вердикт. В качестве бонуса различные драйверы могут выполнять дополнительные действия в случае, если логин успешный. Например, драйвер ORM дополнительно проверяет, есть ли у пользователя права на логин (для этого предусмотрены роли), надо ли запомнить данного пользователя, увеличивает счетчик входов и сохраняет время последнего логина. Кроме того, в сессии (под именем, указанным в config['session_key']) сохраняется объект с данными пользователя (для драйвера ORM это объект класса User_Model).
Проверка авторизации
Конечно, заставлять пользователя вводить логин и пароль на каждой странице глупо. Модуль Auth позволяет проверять, вошел ли на сайт пользователь или он еще пока гость. Для этого есть «висящий» в сессии объект User_Model и метод logged_in().
На самом деле logged_in($role), как видно из имени параметра, позволяет проверить не только сам факт авторизации, но и наличие требуемой роли. Если в сессии присутствует объект User_Model и выполняется условие наличия у данного объекта всех запрошенных ролей (параметр $role может быть и массивом ролей), то возвращается TRUE. Соответственно, можно не передавать имя роли в данный метод, будет анализироваться только содержимое сессии.
Опять же, удобно в конструкторе базового контроллера хранить результат проверки на авторизацию, например так:
$this->user = $this->auth->get_user(); |
Метод get_user() возвращает объект User_Model из сессии, если он там есть. Иначе FALSE.
Выход
Для выхода с сайта используется метод logout($destroy = FALSE), который уничтожает объект User_Model из сессии, а также может и разрушить саму сессию (если параметр $destroy установлен в TRUE). В принципе достаточно вызова logout() без параметров. Метод возвращает TRUE/FALSE, в зависимости от успешного удаления объекта из сессии.
Автологин
Если используется параметр $remember при авторизации, модуль Auth пытается сохранить данные пользователя на указанное в config['lifetime'] время. Вот тут-то нам пригодится стоявшая до сих пор в сторонке модель Auth_User_Token_Model (я не знаю достойного перевода для термина ‘token’, поэтому в дальнейшем буду его использовать как есть). Token по сути представляет из себя некий идентификатор длиной 32 символа, который сохраняется в базе данных и в куках браузера. Таким образом, в пределах времени жизни куки есть возможность сравнить эти значения и если все совпало, то дать возможность автоматически авторизоваться без ввода пароля. Для этого в объекте User_Token_Model есть связь «один-ко-многим» с User_Model (в виде поля user_id).
Дополнительные сведения про token:
- После каждого автоматического входа кука с User_Token_Model обновляется (в целях безопасности меняется идентификатор). Естественно обновляется и запись в БД.
- Примерно каждый сотый token инициирует «чистку мусора» — запрос к БД, удаляющий все записи с истекшим сроком хранения.
- Хранится в куке с именем ‘authautologin‘.
- При выходе (logout) кука удаляется — в следущий раз придется авторизовываться «как все».
Если вы хотите использовать возможность автовхода, сразу после проверки методом logged_in() добавьте вызов метода auto_login():
if ($this->auth->logged_in()) { // пользователь авторизован } elseif ($this->auth->auth_login()) { // у пользователя есть кука и автологин прошел успешно } else { // у нас гость } |
Вход без пароля
В модуле есть и «черный вход» — метод force_login($user), позволяющий без ввода пароля авторизоваться под указанным пользователем. Может быть полезно для взгляда на проект «чужими глазами», но лишний раз играться с такими методами опасно (ИМХО). После такого входа в сессии появляется флаг ‘auth_forced‘, установленный в TRUE. Его надо анализировать перед выполнением каких-либо серьезных изменений в проекте.
Вроде бы все, удачных вам авторизаций!
Даже придраться не к чему
А я решил от Auth отказаться в пользу (ACL, Authen- & Authorization) http://dev.kohanaphp.com/projects/acl
Всего 1 запрос в базу, а гибкость по настройке выше
Я тоже использую A1/A2/Acl, но вот только не всегда удобно использовать роли в виде ENUM поля. Кстати, эта библиотека может использовать и Auth вместо A1.
Разбирался с классическим Auth, в итоге все оказалось не так сложно. Позже, возможно посмотрю и ACL ^_^
Я пробовал использовать A1/ACL и т.д., но все проекты делал с использованием Auth. Не такой уж и тяжелый модуль, если правильно допилять кеширование ролей.
Я вот хотел бы добавить.
Как вы будите регистрировать новых юзверей??? Как будите создавать хеш-соль пароль, изменять их???
Автор забыл сказать о таких методах как
hash_password — получение хеш пароля с солью и без
change_password — смена пароля.
А еще есть вопрос, почему я админ присвоил себе роль админ, указал её в Auth::instance()->logged_in(‘admin’), но увы меня не пускает!!! И только после добавления себе еще роли login я стал входить. Может что-то не так?? Или роль логин должна быть всегда у всех??
1. Созданием пользователей непосредственно библиотека Auth не занимается. Все реализовывается средствами модели Auth_User_Model. В ней при заполнении поля ‘password’ магическим методом _set() происходит хэширование через упомянутый ранее метод Auth::hash_password().
2. Hash_password() я описывал в предисловии, посвященном хэшам и соли. А change_password() относится к модели Auth_User_Model и ничего сложного в нем не происходит — валидация нового пароля и сохранение его в виде хэша.
3. Роль ‘login’ обязательная для авторизации. На самом деле в методе logged_in() идет проверка только на указанную роль, т.е. на ‘admin’, и должна была вернуть TRUE.
Есть маленькая проблема с Auth, после авторизации, в админке, если на пару секунд зажать F5, ну всмысле посылать много запросов один за другим, то меня выкидывает, всмыле теряется авторизация. Только недавно это заметил, потому что если спокойно щелкать по ссылкам в админке, такого не происходит. Никто с таким не сталкивался?
Только в админке или вообще в любом месте на сайте?
Только в админке, то есть только в защищенных Auth контроллерах. При частом многократном вызове страницы, авторизация обрывается.
Интересная информация, попробую потестировать…
Константин, Kohana через какое-то количество запросов (настраивается в конфигах, вроде бы куки) меняет в куках SID. Если часто жать F5, новый sid сгенерируется, запишется в базу, но отправить клиенту не успеет — вас выкинет. Это же может повторяться, если сразу открыть много ссылок
nex2hex
Да, судя по всему, Вы правы. Параметр $config['regenerate'] в session.php по умолчанию равен трем, так что теоретически трех обновлений хватит…
ребята как сделать такую штуку типа кто сейчас на сайте
Создать специальную таблицу user_sessions, в которой хранить идентификаторы пользователей + время последней активности. Время от времени эту таблицу чистить от записей с устаревшими сессиями.
Не могли бы вы написать урок о auth в кохане 3.3.1?
или не могли бы вы мне не грамотному объяснить на почту
комментарий слегка подправил (почту скрыл). [admin]
А что непонятно-то? Куча туториалов по этому модулю, в том числе и в этом блоге. Сам модуль достаточно подробно разобран тут