Построение безопасной системы входа на основе Zend Framework
Вопрос о построении безопасной системы входа в веб-приложение, является довольно распространенным явлением. Аспект безопасности очень важен и в этой статье я покажу вам несколько приемов решения этой задачи.
Давайте построим простое Zend Framework приложение с возможностью авторизации. Регистрационная форма состоит из поля ввода логина и пароля. Эти поля отображены в базе данных MySQL. Структура нашей базы будет выглядеть так:
CREATE TABLE users (
username VARCHAR(32) NOT NULL
, password CHAR(32) NOT NULL
, salt CHAR(20) NOT NULL
, email VARCHAR(80)
, active BOOLEAN NOT NULL DEFAULT 1
, last_access DATETIME NOT NULL
, PRIMARY KEY (username)
);
Как вы видите, мы храним пароль в поле типа CHAR(32). Это потому, что используется хэш-функция MD5(), для безопасного хранения данных. Как вы знаете, если хранить пароли с хэш-функциями, то в теории невозможно инвертировать значение хэш и подобрать пароль. Я написал что в теории, потому что, как пишет Джо Девон в своем комментарии, можно использовать Google для получения наиболее распространенных хэш-значений. К примеру, посмотрите это сайт http://md5.rednoize.com.
В целя повышения безопасности, я использую, так называемую, «соль». Я добавил «соль» в поле типа CHAR(20) в таблицу пользователей со случайно сгенерированной строкой. Строка пароля в MD5 генерируется функцией MD5(CONCAT(salt,’password’)), где ‘password’ это строка с паролем. С «солью» пароль будет более устойчивым к атакам по словарю, состоящему из значений MD5.
С помощью этой системы даже администратор этого сайта не сможет узнать пароли пользователей. Это очень важно с точки зрения конфиденциальности. Вы можете доверять сайтам, которые способны восстановить ваш пароль?
Наше приложение будет использовать классы Zend Framework для создания безопасного входа:
- Zend_Form: создает форму для страницы входа;
- Zend_Auth_Adapter_DbTable: аутентифицирует пользователей хранящихся в базе данных;
- Zend_Session: хранит данные в сессии (Session);
- Zend_Config: хранит конфигурационный файл приложения;
- Zend_Db_Table: карта таблицы базы данных в классе PHP.
Структура приложения
У нас есть структура приложения со следующими каталогами:

В application я храню структуру приложения Zend Framework с файлами bootstrap.php и Initializer.php. Класс Initializer настраивает Front Controller, в котором я описал проверку логина. Я думаю, что это хорошее решение для системы входа, потому что так мы можем управлять системой аутентификации не внося изменений в классы контроллеров. Это значит, что если пользователь отправляет запрос контроллеру, он должен быть уже аутентифицирован.
В папке etc я храню файл config.ini и структуру базы в поддиректории db.
В папку library я кладу общие PHP-классы, которые используются в моих Zend Framework приложениях и поддиректория Forms со всеми формами приложения.
Папка public - это корень приложения с файлами .css, .js, картинки и index.php.
Сейчас мы обратим внимание на плагин Initializer, который является ядром нашей системы входа. Он содержит два важных метода preDispatch и checkSession
/**
* preDispatch
*/
public function preDispatch ()
{
$this->_controller= $this->_front->getRequest()->getControllerName();
if (($this->_controller!='index') && ($this->_controller!='error'))
{
if (Globals::getConfig()->authentication->active)
{
$this->checkSession();
}
}
}
/**
* checkSession
*/
private function checkSession()
{
if (empty(Globals::getSession()->username))
{
$this->_response->setRedirect('/index/login')->sendResponse();
}
}
Метод preDispatch в Zend Framework вызывается первым перед каждым действием, значит есть смысл хранить в нем систему аутентификации. В методе preDispatch мы получаем имя контроллера и проверяем его, если он его имя не является index или error. Если проверка проходит, выполняется метод checkSession.
Заметьте, что я использовал другую проверку в файле config.ini — Globals::getConfig()->authentication->;active если система аутентификации включена. Это удобно, чтобы включать и выключать аутентификацию изменяя только одно значение в файле config.ini.
Контроллеры index и error находятся вне системы аутентификации. Это потому что они содержат страницы, которые должны быть видны без аутентификации.
Метод checkSession очень простой. Он проверяет, если переменная сессии username пустая, то это значит что пользователь не авторизовался и перенаправляет на страницу ввода логина и пароля, т.е. на страницу аутентификации.
Как вы могли заметить, я использовал класс Globals, чтобы получить некоторые одиночные (singleton) объекты связанные с управлением подключения к базе данных, сессии и конфигурационному файлу. Я, обычно, создаю эти синглетоны, чтобы иметь только один объект базы данных, сессии и конфигурационного файла в течении работы моих PHP-приложений.
Кроме того, с синглетонами я могу ускорить чтение из этих объектов в ходе выполнения приложения.
Форма входа
Форма входа приложения описывается в файле library/Forms/Login.php. Я использую класс Zend_Form_Element_Hash чтобы защититься от CSRF-атак (CSRF атаки). Также использую метод setSalt, чтобы повысить безопасность в связке с псевдо-случайной величиной, полученой с функцией md5(uniqid(rand(), TRUE)) и setTimeout для установки «времени жизни» (TTL) (значение timeout определено в файле config.ini).
...
$token = new Zend_Form_Element_Hash('token');
$token->setSalt(md5(uniqid(rand(), TRUE)));
$token->setTimeout(Globals::getConfig()->authentication->timeout);
$this->addElement($token);
...
Это скрытое значение «token» вставляется в форму и дает нам уверенность в том, что данные POST запроса придут с нашей страницы, а не с какой-то другой.
...
public function loginAction()
{
$flash = $this->_helper->getHelper('flashMessenger');
if ($flash->hasMessages())
{
$this->view->message = $flash->getMessages();
}
$this->view->form= new Forms_Login();
$this->render('login');
}
...
public function submitAction()
{
$form= new Forms_Login();
if (!$form->isValid($_POST))
{
if (count($form->getErrors('token')) > 0)
{
return $this->_forward('csrf-forbidden', 'error');
} else
{
$this->view->form = $form;
return $this->render('login');
}
}
$username= $this->getRequest()->getPost('username');
$password= $this->getRequest()->getPost('password');
$authAdapter = new Zend_Auth_Adapter_DbTable(
Globals::getDbConnection(),
'users',
'username',
'password',
'MD5(CONCAT(salt,?)) AND active=1'
);
$authAdapter->setIdentity($username)
->setCredential($password);
$result= $authAdapter->authenticate();
Zend_Session::regenerateId();
if (!$result->isValid())
{
$this->_helper->flashMessenger->addMessage("Ошибка аутентификации.");
$this->_redirect('/index/login');
} else
{
Globals::getSession()->username= $result->getIdentity();
Zend_Loader::loadClass('Users');
$users= new Users();
$data= array ('last_access' => date('Y-m-d H:i:s'));
$where= $users->getAdapter()->quoteInto('username = ?', Globals::getSession()->username);
if (!$users->update($data,$where))
{
throw new Zend_Exception('Error on update last_access');
}
$this->_redirect('/home');
}
}
...
В методе loginAction я создаю форму входа (9-я строка).
Данные страницы входа передаются в метод submitAction. В этом действии мы проверяем валидность форму (16-я строка). Если возникают проблемы с данными «token», это может означать, что на нас проводят CSRF-атаку и мы может перенаправить на специальную страницу ошибки с именем csrf-forbidden. Страница csrf-forbidden возвращает 403 ошибку. «Preventing CSRF properly» of Tom Graham.
Для аутентификации пользователя, мы используем адаптер Zend_Auth_Adapter_DbTable (31-я строка). Мы проверяем авторизацию используя MD5 пароля (MD5(?), где ? это $password) только для активных пользователей (active=1).
Если пользователь авторизован, мы обновляем поле last_access в таблице пользователей и перенаправляем его на главную страницу (строки 55-61).
Если пользователь не авторизован, мы перенаправляем его на страницу входа с сообщением «Ошибка аутентификации».
class HomeController extends Zend_Controller_Action
{
/**
* The default action - show the home page
*/
public function indexAction()
{
$this->view->auth= Globals::getConfig()->authentication->active;
}
/**
* logout
*/
public function logoutAction()
{
if (Zend_Session::sessionExists())
{
Zend_Session::destroy(true,true);
$this->_redirect('/index/login');
}
}
}
Заметьте, что регенерировали id сессии с помощью Zend_Session::regenerateId() во время входа (строка 43) и очистили сессию во время выхода (logout) (строка 14 метода logoutAction в HomeController). Это сделано из соображений безопасности, чтобы уменьшить возможность проведения session fixation-атак.
В этом посте, я не буду рассматривать SQL-инъекции. Почему? Потому что класс Zend_Db, который мы исползуем в приложении использует квотирование для предотвращения таких атак. Более того, мы использовали валидатор Zend_Form, чтобы фильтровать входные значения имени пользователя и пароля.
Заключение
В этом посте я показал безопасный вход в приложение с применением Zend Framework.
Важные аспекты безопасности:
- MD5 + «соль» паролей, хранящихся в базе данных;
- генерация псевдо-случайного ключа (token) в форме, для предотвращения CSRF атак.
- timeout ключа (token) для повышение безопасности системы входа;
- регенерация Id сессии для смягчения возможности session fixation атак;
- перенаправления на страниц ошибки 403 для предотвращения CSRF атак;
- класс Zend_DB использует квотирование, для предотвращения SQL-иньекций.
Один из небезопасных моментов заключается в том, что имя пользователя и пароль передаются в текстовом формате. Любой злоумышленник может перехватить эти данные сниффером. В целях создания надежной системы используется протокол Secure Sockets Layer (SSL). На данные момент, это единственный способ шифрования связи между клиентом и пользователем.
Популярность: 38%
Если у вас возникли вопросы, вы можете оставить их в комментариях
Корпусная мебель на заказ по доступным ценам. . работа в Киеве прорабом: http://rabota.slando.com.ua/kiev/16860_1.html

Евгений, могли бы Вы прислать архив с кодом этого проекта. Не все понятно, надо посмотреть весь код.
Спасибо!
То что нужно, с одним «НО».
Подобная проблема появляется как правило у новичков, таких как я. Поэтому желательно давать не просто вырезки из классов, а полностью классы. А ещё лучше подкреплять теорию архивом с кодом.
Вышлите пожалуйста рабочий пример, очень прошу, уже всё облазил так и не понял как мне не прописывать проверку логина в каждом контроллере а делать это афтоматом во всех контролерах.
Заранее спасибо!
Жду.
Отличная статья, Евгений, но, как уже указывалось выше в комментариях — не все понятно без исходников целиком.
Если Вас не затруднит — пришлите, пожалуйста, исходники, или, если есть возможность, выложите их для общего доступа.
Заранее спасибо!
Исходники выложить не могу. Высылаю по просьбе на мыло.
Хорошая статья. А как передавать форму используя протокол Secure Sockets Layer (SSL)? Может выложите пример.
Спасибо за статью. Хотелось бы увидеть исходники работающего примера (только изучаю ZF).
Добрый день. Спасибо за познавательную статью. Хотелось бы увидеть исходники.
Здорово сделано. Можно ли получить архив?
Заранее благодарен.
А если бы была ссылка на donation, обязательно пополнил бы.
Интересная статья. Я работал с ZF несколько раз, вот пока до системы авторизации на нем же не дошел…
В общем, хотелось бы увидеть полный код:)
Стоит упомянуть оригинал, на основе которого создана эта статья.
Build a secure login with Zend Framework
http://www.zimuel.it/blog/?p=86
Жаль, что перевод унаследовал все недостатки оригинала. Но всё-равно спасибо.
>NobodyInParticular
у английского варианта есть выложенные исходники:
http://www.zimuel.it/blog/wp-content/uploads/2009/11/zfsecurelogin.zip
Статья 5! Но, нету исходников.
Пожалуйста, поделитесь исходниками!
Заранее благодарен!
Интересно, что человек пишет статью о построении системы входа на ZendFramework, но сам использует движок вордпресса:)))))
Как уже сказали выше, статья просто перевод. На зенде писал, но не много. А с блогом, просто не вижу смысла заморачиваться. Надеюсь вы меня поймете.
)))
Отличная статья! А можно мне ваши исходники на мыло? Тока начинаю изучать ZF и не все понятно.
Спасибо, статья хорошая!)
Здравствуйте, Евгений,
спасибо за качественный перевод. К сожаленую на сайте у автора тоже не удалось скачать программный код.
Не могли бы мне выслать на майл?
Заранее благодарю.
Можите выслать выслать исходники. только начинаю осваивать ZF.
Здравствуйте!
Не могли бы Вы скинуть исходники проекта.
Заранее спасибо.
Добрый день! Поделитесь исходниками пожалуйста. Благодарю.