UserController
работает в паре с сервисом UserManager, содержащим всю бизнес-логику, связанную с управлением пользователями.
Этот сервис позволяет создавать и обновлять пользователей, а также изменять и сбрасывать пароль пользователя. Некоторые части данного
сервиса мы рассмотрим более детально, а некоторые, наиболее очевидные, пропустим (напоминаем, что готовый код можно посмотреть в
образце User Demo).
Добавить нового пользователя с помощью метода addUser()
класса UserManager
. Выглядит он следующим образом:
/**
* Этот метод добавляет нового пользователя.
*/
public function addUser($data)
{
// Не допускаем создание нескольих пользователей с одинаковым адресом эл. почты.
if($this->checkUserExists($data['email'])) {
throw new \Exception("User with email address " .
$data['$email'] . " already exists");
}
// Создаем новую сущность User.
$user = new User();
$user->setEmail($data['email']);
$user->setFullName($data['full_name']);
// Зашифровываем пароль и храним его в зашифрованном состоянии.
$bcrypt = new Bcrypt();
$passwordHash = $bcrypt->create($data['password']);
$user->setPassword($passwordHash);
$user->setStatus($data['status']);
$currentDate = date('Y-m-d H:i:s');
$user->setDateCreated($currentDate);
// Добавляем сущность в менеджер сущностей.
$this->entityManager->persist($user);
// Применяем изменения к базе данных.
$this->entityManager->flush();
return $user;
}
В данном методе мы первым делом проверяем, существует ли другой пользователь с таким же адресом электронной почты (строка 7). Если существует, мы запрещаем создание нового пользователя, выбросив исключение.
Если пользователя с таким адресом не существует, мы создаем новую сущность User
(строка 13) и задаем
ее свойства.
Обратите внимание на то, как мы сохраняем в базу данных пароль пользователя. Из соображений безопасности
мы не сохраняем его как есть, а вычисляем его хэш с помощью класса Bcrypt
, предоставляемого компонентом
Zend Framework под названием Zend\Crypt
(строки 18-19).
Zend\Crypt
можно установить следующей командой:
php composer.phar require zendframework/zend-crypt
Компонент
Zend\Crypt
также требует установленного PHP-расширенияmcrypt
.
Bcrypt - широко используемый алгоритм хэширования, рекомендуемый сообществом информационной безопасности для хранения паролей пользователей. Шифрование паролей с помощью
Bcrypt
на данный момент считается безопасным. Некоторые разработчики до сих пор используют MD5 или SHA1 с солью, но эти алгоритмы больше не считаются безопасными (MD5 и SHA1 могут быть взломаны).
Когда пользователь заходит на свой аккаунт, вам необходимо сравнить хэш, хранящийся в БД,
и хэш, вычисленный от введенного пользователем пароля. Это можно сделать с помощью метода
verify()
класса Bcrypt
, как показано ниже:
/**
* Проверяет, что заданный пароль является корректным..
*/
public function validatePassword($user, $password)
{
$bcrypt = new Bcrypt();
$passwordHash = $user->getPassword();
if ($bcrypt->verify($password, $passwordHash)) {
return true;
}
return false;
}
Следующим важным аспектом UserManager
является создание пользователя Admin.
Admin - это первоначальный пользователь, создающийся автоматически, когда в базе данных еще нет пользователей, и позволяющий вам в первый раз войти на сайт.
/**
* Этот метод проверяет, существует ли хотя бы один пользователь, и, если таковых нет, создает
* пользователя 'Admin' с эл. адресом 'admin@example.com' и паролем 'Secur1ty'.
*/
public function createAdminUserIfNotExists()
{
$user = $this->entityManager->getRepository(User::class)->findOneBy([]);
if ($user==null) {
$user = new User();
$user->setEmail('admin@example.com');
$user->setFullName('Admin');
$bcrypt = new Bcrypt();
$passwordHash = $bcrypt->create('Secur1ty');
$user->setPassword($passwordHash);
$user->setStatus(User::STATUS_ACTIVE);
$user->setDateCreated(date('Y-m-d H:i:s'));
$this->entityManager->persist($user);
$this->entityManager->flush();
}
}
Мы задаем адрес эл. почты пользователя Admin как admin@example.com
, а пароль - как Secur1ty
. Таким образом,
вы можете первый раз войти на сайт с помощью этих идентификационных данных.
Иногда пользователи забывают свои пароли. В таком случае нам нужно предоставить им возможность сбросить пароль - безопасно его изменить. Сброс пароля работает следующим образом:
Вы обычно не храните токены сброса пароля в чистом виде в БД. Вместо этого вы храните хэш токена. Это делается в целях безопасности. Даже если некий злоумышленик украдет БД, они не смогут сбросить пароли пользователей.
Алгоритм генерации токена сброса пароля реализован внутри метода generatePasswordResetToken()
класса UserManager
.
Чтобы сгенерировать случайную строку, мы используем класс Rand
, предоставляемый компонентом Zend\Math
.
/**
* Генерирует для пользователя токен сброса пароля. Этот токен хранится в базе данных и
* отсылается на адрес эл. почты пользователя. Когда пользователь нажимает на ссылку в сообщении,
* он направляется на страницу Set Password.
*/
public function generatePasswordResetToken($user)
{
if ($user->getStatus() != User::STATUS_ACTIVE) {
throw new \Exception('Cannot generate password reset token for inactive user ' . $user->getEmail());
}
// Generate a token.
$token = Rand::getString(32, '0123456789abcdefghijklmnopqrstuvwxyz', true);
// Encrypt the token before storing it in DB.
$bcrypt = new Bcrypt();
$tokenHash = $bcrypt->create($token);
// Save token to DB
$user->setPasswordResetToken($tokenHash);
// Save token creation date to DB.
$currentDate = date('Y-m-d H:i:s');
$user->setPasswordResetTokenCreationDate($currentDate);
// Apply changes to DB.
$this->entityManager->flush();
// Send an email to user.
$subject = 'Password Reset';
$httpHost = isset($_SERVER['HTTP_HOST'])?$_SERVER['HTTP_HOST']:'localhost';
$passwordResetUrl = 'http://' . $httpHost . '/set-password?token=' . $token . "&email=" . $user->getEmail();
// Produce HTML of password reset email
$bodyHtml = $this->viewRenderer->render(
'user/email/reset-password-email',
[
'passwordResetUrl' => $passwordResetUrl,
]);
$html = new MimePart($bodyHtml);
$html->type = "text/html";
$body = new MimeMessage();
$body->addPart($html);
$mail = new Mail\Message();
$mail->setEncoding('UTF-8');
$mail->setBody($body);
$mail->setFrom('no-reply@example.com', 'User Demo');
$mail->addTo($user->getEmail(), $user->getFullName());
$mail->setSubject($subject);
// Setup SMTP transport
$transport = new SmtpTransport();
$options = new SmtpOptions($this->config['smtp']);
$transport->setOptions($options);
$transport->send($mail);
}
Настройка почтовой системы для веб-сервера, как правило, требует покупки платной подписки почтового сервиса (такого как SendGrid или Amazon SES)
Валидация токена сброса пароля реализована внутри метода validatePasswordResetToken()
.
Мы сверяем передаваемый токен с тем, что мы сохранили в базе данных, а также проверяем,
что срок действия токена (1 день после создания) не истек .
/**
* Проверяем, действителен ли токен сброса пароля.
*/
public function validatePasswordResetToken($email, $passwordResetToken)
{
// Find user by email.
$user = $this->entityManager->getRepository(User::class)
->findOneByEmail($email);
if($user==null || $user->getStatus() != User::STATUS_ACTIVE) {
return false;
}
// Check that token hash matches the token hash in our DB.
$bcrypt = new Bcrypt();
$tokenHash = $user->getPasswordResetToken();
if (!$bcrypt->verify($passwordResetToken, $tokenHash)) {
return false; // mismatch
}
// Check that token was created not too long ago.
$tokenCreationDate = $user->getPasswordResetTokenCreationDate();
$tokenCreationDate = strtotime($tokenCreationDate);
$currentDate = strtotime('now');
if ($currentDate - $tokenCreationDate > 24*60*60) {
return false; // expired
}
return true;
}
И наконец, setPasswordByToken()
позволяет установить новый пароль для пользователя.
/**
* Этот метод устанавливает новый пароль по токену сброса пароля.
*/
public function setNewPasswordByToken($email, $passwordResetToken, $newPassword)
{
if (!$this->validatePasswordResetToken($email, $passwordResetToken)) {
return false;
}
// Find user with the given email.
$user = $this->entityManager->getRepository(User::class)
->findOneByEmail($email);
if ($user==null || $user->getStatus() != User::STATUS_ACTIVE) {
return false;
}
// Set new password for user
$bcrypt = new Bcrypt();
$passwordHash = $bcrypt->create($newPassword);
$user->setPassword($passwordHash);
// Remove password reset token
$user->setPasswordResetToken(null);
$user->setPasswordResetTokenCreationDate(null);
$this->entityManager->flush();
return true;
}