Безопасное хранение пароля в модели Yii
Достаточно часто на тематических форумах встречаются вопросы новичков об организации хранения паролей пользователей в модели User. Вопрос звучит примерно так: «У меня в модели User есть поле password, в котором я храню хэш пароля. Как мне сделать так, чтобы пароль не перезаписывался при каждом сохранении модели?» Вопрос любопытный и для других фреймворков.
Действительно, многие начальные мануалы советуют хэшировать пароль перед первым сохранением модели (в методе beforeSave):
class User extends CActiveRecord { // ... protected function beforeSave() { if (parent::beforeSave()) { // если это новая запись if ($this->isNewRecord) { $this->salt = $this->generateSalt(); $this->password = $this->hashPassowrd($this->password, $this->salt); } return true; } else return false; } // ... }
Это самый примитивный способ не позволяет легко менять пароль в дальнейшем без непосредственного присваивания хеша пароля в коде контроллера. Мы же стремимся сохранить контроллер примитивным:
protected function actionSettings() { $model = $this->loadModel(); $model->scenario = 'settings'; // указываем нужный сценарий для правил валидации if (isset($_POST['User']) { $model->attributes = $_POST['User']; if ($model->save()) { Yii::app()->user->setFlash('settings-form', 'Сохранено!'); $this->refresh(); } } $this->render('settings', array('model'=>$model)); }
Как вариант, можно запоминать старый пароль в приватной переменной и сравнивать с новым при каждом сохранении. Если пароль другой, то заменить его на хэш:
class User extends CActiveRecord { private $_old_password; // для хранения хэша старого пароля // ... protected function afterFind() // при чтении из базы { $this->_old_password = $this->password; parent::afterFind(); } protected function beforeSave() { if (parent::beforeSave()) { if ($this->password !== $this->_old_password) { $this->salt = $this->generateSalt(); $this->password = $this->hashPassword($this->password, $this->salt); } return true; } else return false; } // ... }
Вроде бы всё работает, но попробуйте вывести форму
<?php $form=$this->beginWidget('CActiveForm'); echo $form->textField($model,'username'); <br /> echo $form->textField($model,'email'); <br /> echo $form->passwordField($model,'password'); <br /> echo CHtml::submitButton('Сохранить'); $this->endWidget();
...и Вы увидите, что поле пароля будет заполнено кучей точек. Там действительно будет содержимое поля $model->password. Таким образом, в поля формы будут выведены текущий логин, email и хэш пароля, а это небезопасно.
Мы должны иметь возможность задавать новый пароль и не должны иметь прямого доступа из формы к старому!
Добавим в модель пару полей:
class User extends CActiveRecord { public $new_password; public $new_confirm; public function rules() { return array( array('username, email', 'required'), array('username', 'match', 'pattern'=>'#^[a-zA-Z0-9_\.-]+$#', 'message'=>'Логин содержит запрещённые символы'), array('email', 'email', 'message'=>'Неверный формат E-mail адреса'), array('username, email', 'unique', 'caseSensitive'=>false), array('email, username, new_password, new_confirm', 'length', 'max'=>255), array('new_password', 'length', 'min'=>6, 'allowEmpty'=>true), array('new_confirm', 'compare', 'compareAttribute'=>'new_password', 'message'=>'Пароли не совпадают'), // Register array('new_password', 'required', 'on'=>'register'), // Settings array('username', 'unsafe', 'on'=>'settings'), ); } public function attributeLabels() { return array( // ... 'new_password' => 'Новый пароль', 'new_confirm' => 'Подтвердите новый пароль', ); } protected function beforeSave() { if (parent::beforeSave()) { if ($this->new_password) { $this->salt = $this->generateSalt(); $this->password = $this->hashPassword($this->new_password, $this->salt); } return true; } else return false; } }
Здесь мы добавили отдельное поле для ввода нового пароля. Исходное поле password
объявили небезопасным (его теперь нельзя перезаписать через безопасное присваивание атрибутов). В методе beforeSave()
модели мы смотрим, введён ли новый пароль в поле new_password
. Если введён, то записываем его хэш в поле password
для записи в БД.
Теперь в формах регистрации (сценарий «register») и настроек профиля (сценарий «settings») нужно использовать поле new_password вместо password;
<?php $form=$this->beginWidget('CActiveForm'); echo $form->textField($model,'username'); <br /> echo $form->textField($model,'email'); <br /> echo $form->passwordField($model,'new_password'); <br /> echo $form->passwordField($model,'new_confirm'); <br /> echo CHtml::submitButton('Сохранить'); $this->endWidget();
Если теперь ввести пароль в это поле, то он правильно сохранится в модели, а поле ввода пароля во всех формах будет пустым. Контроллер вообще не изменился. Всю работу делает модель.
Спасибо за эту и другие статьи, особенно по Yii! У Вас отличный блог: статьи, идеи, дизайн, изложение материала - все на высшем уровне. Большая редкость. Удачи Вам!
Спасибо! Я долго к этому шёл.
Дмитрий. Вопрос не в тему, но всеравно спрошу.
Как у Вас реализована подсветка синтаксиса кода?
Наверно с помощью CTextHighlighter, правильно?
Но меня интересует как работет сам вывод?
Вы сохраняете в базу уже отворматированый код с помощью класа, или сохраняете в базу с метками например
Если с метками то наверно парсите контент и пропускаете все что между этими тегами через CTextHighlighter. Правильно я понимаю?
Использую в блоге Markdown синтаксис вместо HTML. Преобразование в HTML c подсветкой делает встроенный в Yii компонент CMarkdown. В базе имеется два поля: оригинальный текст и переконвертированный. Вывожу текст из второго поля, не забыв вручную в представлении вызвать CTextHighlighter::registerCssFile(). Для удобства использую поведение DPurifyTextBehavior с включенной опцией enableMarkdown. Оно конвертирует из Markdown в HTML при сохранении модели.
Спасибо. Не знал.
Очень интересно, посмотрю.
Нужная статья была бы по интеграции такого решения, что скажете ;)
Аналогов вашему сайту вообще нет в рунете, спасибо.
И как всегда спасибо Дмитрий!
Дмитрий, спасибо, за статью. Там в rules наверное опечатка. Вместо array('username', 'unsafe', 'on'=>'settings') нужно 'password'.
Нет, всё правильно. Password в правилах не уазан, поэтому он и так будет unsafe. А это правило сделано специально, чтобы пользователь не смог поменять свой ник.
Скажите, а есть ли какие-то готовые рецепты для регистрации пользователя и восстановления пароля?
Было странно узнать, что решить эту задачу для меня оказалось гораздо сложнее, чем написать остальной сайт. Задача ведь, в принципе, стандартная.
Лучше изменю свой вопрос (а то в предыдущем посте я, по сути, просил готовый код). Как лучше создать ссылку для восстановления пароля?
На ум приходит что-то следующее:
1) создавать отдельную таблицу с эл. адресами пользователей и кодами восстановления. Записи в таблицу будут производиться при запросе на восстановление пароля и введения эл. адреса существующего пользователя.
В таблице будет два столбца: введенный эл. адрес и случайный уникальный код.
2) после добавления записи в таблицу, по эл. адресу высылается письмо со ссылкой на соответствующее действие контроллера, передающей код и эл. адрес методом get.
3) если контроллер обнаруживает совпадение адреса и кода, то выводит форму для смены пароля.
Можно не создавать таблицу, а добавить столбец для кода восстановления к таблице с пользователями, но это мне кажется менее рациональным.
PS Надеюсь, что я хотя бы понятно изложил свою мысль. Я изучаю Yii меньше месяца, многое непонятно. Например код из статьи выше я слабо понял. Надеюсь на доброту к новичкам :)
Для меня логичнее делать подтверждение регистрации и восстановление пароля дополнительными полями confirm_code и reset_code (со случайным md5 кодом) и отправкой простой ссылки с этим кодом. А в контроллере ищем прямо по этому коду, обновляем пароль и стираем код.
А так можно подсмотреть в расширении yii-user или yii2-user.
Добрый день! Очень интересно, хочу поинтересоваться - как изменять под необходимые экшены виджет в column2
Например в некоторых экшенах я хочу чтобы его совсем не было, а в некоторых нужно, чтобы показывались определенные пункты, по умолчанию почему - то create и manage
Пункты $this->menu задаются в каждом представлении экшэнов. Просто удалите ненужные.
Дмитрий, подскажите пожалуйста.
Есть проблема. В проекте реализовано так что админ самостоятельно регистрирует пользователя, и немного неудобно каждый раз придумывать пароли. Есть идея сделать генератор пароля с задаваемой длиной.
в модели:
в контролере
в итоге: в $hash передается NULL. что я делаю не так?
Что-то не понял, где у Вас какой код.