Askeet. День шестой. Безопасность и проверка форм.
Приветствуем!
Хотите что-то написать?Нужно назвать себя.
Если вы пришли в первый раз,
то нужно зарегистрироваться.
Переводы → Askeet. День шестой. Безопасность и проверка форм.
Ранее на symfony
В течение пятого дня Вы научились использовать шаблоны и экшены, формы и пэджеры теперь от вас также ничего не скрывают. Но после создания формы авторизации, Вы вероятно хотели бы услышать как ограничить возможности неавторизированного пользователя. Этим то мы сегодня и займёмся, а заодним посмотрим на работу валидации форм. Т.к. мы будем расширять приложение с помощью собственных классов, неплохо было бы ознакомиться с главой Inside the View Layer в книге по symfony.
Проверка формы авторизации
Файл для проверки
Форма авторизации имеет два поля: nickname и password. Но что будет если пользователь введёт неверные данные? Предусмтрим этот случай и создадим файл login.yml в папке /frontend/modules/user/validate (login - имя экшна проверки). Добавим следущее:
methods:
post: [nickname, password]
names:
nickname:
required: true
required_msg: your nickname is required
validators: nicknameValidator
password:
required: true
required_msg: your password is required
nicknameValidator:
class: sfStringValidator
param:
min: 5
min_error: nickname must be 5 or more characters
Вначале под заголовком methods определяем список полей для проверки для метода формы (мы здесь определяем только для метода POST, потому что GET отображает форму и требует проверки). Затем идёт заголовок names с требованиями для каждого из перечисленных полей, а также сообщение об ошибке. В конце концов, т.к. для поля nickname объявлен определённый набор правил проверки, они детально описываются ниже под соответствующим заголовком. В этом примере sfStringValidator - встроеный валидатор symfony, который проверяет формат строки (валидаторы symfony рассмотрены в главе Forms в книге по symfony).
Обработка ошибок
Что же произойдёт если пользователь введёт неверные данные? Условия, описанные в файле login.yml, не выполнятся, и контроллер symfony передаст запрос в метод handleErrorLogin() в класс userActions вместо метода executeLogin(), как планировалось в аргументе к form_tag. Если этот метод не существует, по умолчанию отобразится шаблон loginError.php, потому что стандартный метод handleError() возвращает:
public function handleError(){ return sfView::ERROR;}
Но это целый шаблон для вывода, а нам нужно отобразить форму авторизации заново с сообщениями об ошибке рядом с невернозаполненными полями. Что ж, давайте изменим поведение при ошибке авторизации на отображение, в нашем случае, шаблона loginSuccess.php:
public function handleErrorLogin(){ return sfView::SUCCESS;}
Соглашение об именовании названий экшнов, возвращаемых ими значений и файлов шаблонов описано в главе Inside the View Layer в книге по symfony.
Шаблон хелпера ошибок
Опять вызываем шаблон loginSuccess.php, на этот раз для отображения ошибки. Для этого используем хелпер form_error() из группы хелперов Validation. Испровьте два form-row div'а следущим образом:
<?php use_helper('Validation') ?> <div class="form-row"> <?php echo form_error('nickname') ?> <label for="nickname">nickname:</label> <?php echo input_tag('nickname', $sf_params->get('nickname')) ?></div> <div class="form-row"> <?php echo form_error('password') ?> <label for="password">password:</label> <?php echo input_password_tag('password') ?></div>
Хелпер form_error() выдаст сообщение об ошибке, определённое в login.yml, если ошибка укзана в поле как параметр.
Пришло время проверить валидацию формы, пытаясь ввести ник короче 5 символов или пропуская несколько полей. Сообщение об ошибке волшебным образом отобразится над нужным полем:

Пароль необходим, но его нет в базе данных! Это неважно, т.к. введя любой пароль Вы успешно залогинетесь. Не очень безопасный процесс, правда?
Стили ошибок
Если при проверке формы Вы получили ошибку, Вы вероятно заметили, что ваша ошибка не оформлена так же как заголовок выше. Так получилось, потому что мы определили класс .form_error (в файле web/main.css), который является классом по умолчанию для ошибок в формах, сгенерированных хелпером form_error():
.form_error{ padding-left: 85px; color: #d8732f;}
Авторизация пользователя
Собственный валидатор
Помните вчерашнюю проверку существования введённого ника в экшне login? Вот, это и есть валидация формы. Этот код нужно взять из экшна и вставить в свой валидатор. Вы думаете это сложно? Вовсе нет. Отредактируйте файл валидации login.yml так:
...
names:
nickname:
required: true
required_msg: your nickname is required
validators: [nicknameValidator, userValidator]
...
userValidator:
class: myLoginValidator
param:
password: password
login_error: this account does not exist or you entered a wrong password
Мы просто добавили валидатор класса myLoginValidator для поля nickname. Этот валидатор ещё не существует, но мы знаем, что ему будет нужен пароль для полной авторизации пользователя, он передаётся как параметр с меткой password.
Хранилище пароля
Но подождите минутку. В нашей моделе, как и в тестовых данных, не указан пароль. Самое время задать его. Но Вы знаете что хранение пароля в чистом виде в базе данных - плохая идея с точки зрения безопасности. Мы будем хранить sha1 хэш пароля, а также случайный ключ, использованный для его генерации. Если Вы не знакомы с использованием 'salt', посмотрите методы взлома паролей.
Теперь откроем schema.xml и добавим следущие столбцы в таблицу User:
<column name="email" type="varchar" size="100" /><column name="sha1_password" type="varchar" size="40" /><column name="salt" type="varchar" size="32" />
Пересоздадим модель Propel: symfony propel-build-model. Вы должны также добавить два столбца в базу данных, либо вручную, либо используя lib.model.schema.sql сгенерированный после symfony propel-build-sql. Теперь откроем askeet/lib/model/User.php и добавим метод setPassword():
public function setPassword($password){ $salt = md5(rand(100000, 999999).$this->getNickname().$this->getEmail()); $this->setSalt($salt); $this->setSha1Password(sha1($salt.$password));}
Эта функция имитирует прямое хранение пароля, но вместо этого она хранит случайный ключ salt (32-символьный хэш случайной строки) и хэш пароля (40-символьная строка).
Добавление пароля в тестовые данные
Помните файлы тестовых данных из третьего дня? Пришло время добавить пароль и email к тестовому пользователю. Откройте и исправьте askeet/data/fixtures/test_data.yml как показано ниже:
User:
...
fabien:
nickname: fabpot
first_name: Fabien
last_name: Potencier
password: symfony
email: fp@example.com
francois:
nickname: francoisz
first_name: François
last_name: Zaninotto
password: adventcal
email: fz@example.com
Как указано в методе setPassword() класса User, объект sfPropelData правильно заполнит столбцы sha1_password и salt, определённые в схеме когда мы вызовем:
$ php batch/load_data.php
Заметьте, что объект
sfPropelDataможет работать с методами, которые связываны с реальными записями в базе данных (и теперь мы опередили Ваш традиционный SQL-дамп!).Как это возможно? Посмотрите главу Inside the Model Layer в книге по symfony.
Заметка: Не обязательно задавать пароль пользователю 'Anonymous Coward', т.к. мы запретим его авторизацию. И мы очень надеемся, что вы не пробовали ставить эти два пароля на ваши баноковские аккаунты, потому что они конфиденциальны!
Собственный валидатор
Пришло время написать тот самый myLoginValidator. Вы можете создать его в любой директории lib/, которая доступна для модулей (такой как askeet/lib/ или askeet/apps/frontend/lib/ или askeet/apps/frontend/modules/user/lib/). Т.к. мы создаём валидатор для приложения, создадим myLoginValidator.class.php в директории askeet/apps/frontend/lib/:
<?php class myLoginValidator extends sfValidator{ public function initialize($context, $parameters = null) { // initialize parent parent::initialize($context); // set defaults $this->setParameter('login_error', 'Invalid input'); $this->getParameterHolder()->add($parameters); return true; } public function execute(&$value, &$error) { $password_param = $this->getParameter('password'); $password = $this->getContext()->getRequest()->getParameter($password_param); $login = $value; // anonymous is not a real user if ($login == 'anonymous') { $error = $this->getParameter('login_error'); return false; } $c = new Criteria(); $c->add(UserPeer::NICKNAME, $login); $user = UserPeer::doSelectOne($c); // nickname exists? if ($user) { // password is OK? if (sha1($user->getSalt().$password) == $user->getSha1Password()) { $this->getContext()->getUser()->setAuthenticated(true); $this->getContext()->getUser()->addCredential('subscriber'); $this->getContext()->getUser()->setAttribute('subscriber_id', $user->getId(), 'subscriber'); $this->getContext()->getUser()->setAttribute('nickname', $user->getNickname(), 'subscriber'); return true; } } $error = $this->getParameter('login_error'); return false; }}
Когда требуется валидатор - после отправки формы login - сначала вызывается метод initialize(). Он инициализирует стандартное значение сообщение login_error ('Invalid Input') и прикрепляет параметры (те, что указаны под заголовком param: в файле login.yml) к объекту.
Затем вызываем метод execute(). $password_param - это имя поля, заданное в login.yml вод заголовком password. Оно используется как имя поля для получения значения из параметров запроса. Так $password содержит пароль, введённый пользователем. $value получает значение текущего поля, а класс myLoginValidator вызывается для поля nickname. Так $login содержит ник, введённый пользователем. Наконец-то! Теперь у валидатора есть все неодходимые данные для корректной проверки пользователя.
Следущий код был взят из экшна login. Кроме того, произведена проверка пароля (раньше всегда true): Хэш пароля, ведённого пользователем (используя salt из базы данных) сравнивается с хэшем пользователя.
Если логин и пароль корректны, валидатор вернёт true и будет вызван экшн формы (executeLogin()). Если нет - вернёт false и вызовет handleErrorLogin().
Удаление кода из экшна
Теперь весь код проверки находится в валидаторе, нам нужно удалить его из экшна login. Действительно, когда экшн вызывается методом POST, это значит что валидатор проверил запрос и пользователь корректный. Это означает, что единственное, что должен сделать экшн в этом случае, это переадресовать на страницу referer:
public function executeLogin(){ if ($this->getRequest()->getMethod() != sfRequest::POST) { // display the form $this->getRequest()->getParameterHolder()->set('referer', $this->getRequest()->getReferer()); return sfView::SUCCESS; } else { // handle the form submission // redirect to last page return $this->redirect($this->getRequestParameter('referer', '@homepage')); }}
Провертее изменения, пытаясь авторизоваться одним из тестовых пользователей (после чистки кэша, т.к. мы создали новый класс валидатора, который должен автоматически подгружаться).
Ограничение доступа
Если вы хотите ограничить доступ к экшну, Вам про сто нужно добавить security.yml в директорию config/ модуля, как показано ниже (не делайте этого сейчас):
all:
is_secure: on
credentials: subscriber
Экшны этого модуля будут запущены только авторизированным пользователем с правами subscriber.
В "Спроси-ка", авторизация нужна для отправки новых вопросов, отметки интересных вопросов и проставления рейтинга комментариев. Все остальные экшны доступны так же неавторизированным пользователям.
Чтобы ограничить доступ к экшну question/add (для записи), добавьте следущий файл security.yml в директорию askeet/apps/frontend/modules/question/config/:
add:
is_secure: on
credentials: subscriber
all:
is_secure: off
Чуть-чуть порефакторим?
День почти закончен, но мы хотели бы немного поиграть в нашу любимую игру "поместим-код-в-неприемлемое-место".
Четыре строки кода, которые вызываются, когда пароль пользователя проверен и сохраняют его id для следущих запросов. Вы наверно видели их как метод класса myUser (класс сессии, не класс User, соответствующий столбцу User). Это легко сделать. Добавьте следущий метод в класс askeet/apps/frontend/lib/myUser.php:
public function signIn($user){ $this->setAttribute('subscriber_id', $user->getId(), 'subscriber'); $this->setAuthenticated(true); $this->addCredential('subscriber'); $this->setAttribute('nickname', $user->getNickname(), 'subscriber');} public function signOut(){ $this->getAttributeHolder()->removeNamespace('subscriber'); $this->setAuthenticated(false); $this->clearCredentials();}
Теперь измените четыре строки, начинающиеся с $this->getContext()->getUser() в классе myLoginValidator на:
$this->getContext()->getUser()->signIn($user);
Также измените экшн user/logout (Вы же не забыли про него?):
public function executeLogout(){ $this->getUser()->signOut(); $this->redirect('@homepage');}
Аттрибуты сессии subscriber_id и nickname так же могут быть получены через методы-геттеры. Так же в классе myUser добавьте три следущих метода:
public function getSubscriberId(){ return $this->getAttribute('subscriber_id', '', 'subscriber');} public function getSubscriber(){ return UserPeer::retrieveByPk($this->getSubscriberId());} public function getNickname(){ return $this->getAttribute('nickname', '', 'subscriber');}
Вы можете воспользоваться одним из новых методов в layout.php: измените строку
<li><?php echo link_to($sf_user->getAttribute('nickname', '', 'subscriber').' profile', 'user/profile') ?></li>
на
<li><?php echo link_to($sf_user->getNickname().' profile', 'user/profile') ?></li>
Не забудьте проверить изменения. Тот же самы процесс авторизации всё ещё работает, но теперь с лучшим кодом.
Увидимся завтра
Завтра немного поработаем над настройкой внешнего вида, поправим CSS, компоненты, а также заголовки страниц.
Не забывайте, что можете скачать сегодняшний код из репозитория "Спроси-ка", с отметкой release_day_6. Если вы хотите задать или ответить на вопрос о "Спроси-ка", не стеснятесь заходить на форум "Спроси-ка". Не забывайте что программа 21 дня всё ещё ждёт Вас.
Коментарии:
очень хотелось бы увидеть 2 предедущие главы в переводе. я какраз на них сижу :)
ответить



