A free and open-source book on ZF3 for beginners


16.6. Добавление сервиса UserManager

UserController работает в паре с сервисом UserManager, содержащим всю бизнес-логику, связанную с управлением пользователями. Этот сервис позволяет создавать и обновлять пользователей, а также изменять и сбрасывать пароль пользователя. Некоторые части данного сервиса мы рассмотрим более детально, а некоторые, наиболее очевидные, пропустим (напоминаем, что готовый код можно посмотреть в образце User Demo).

16.6.1. Создание нового пользователя и хранение пароля в зашифрованном виде

Добавить нового пользователя с помощью метода 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 могут быть взломаны).

16.6.2. Валидация зашифрованного пароля

Когда пользователь заходит на свой аккаунт, вам необходимо сравнить хэш, хранящийся в БД, и хэш, вычисленный от введенного пользователем пароля. Это можно сделать с помощью метода verify() класса Bcrypt, как показано ниже:

/**
 * Проверяет, что заданный пароль является корректным..
 */
public function validatePassword($user, $password) 
{
    $bcrypt = new Bcrypt();
    $passwordHash = $user->getPassword();
    
    if ($bcrypt->verify($password, $passwordHash)) {
        return true;
    }
    
    return false;
}

16.6.3. Создание пользователя Admin

Следующим важным аспектом 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. Таким образом, вы можете первый раз войти на сайт с помощью этих идентификационных данных.

16.6.4. Сброс пароля пользователя

Иногда пользователи забывают свои пароли. В таком случае нам нужно предоставить им возможность сбросить пароль - безопасно его изменить. Сброс пароля работает следующим образом:

Вы обычно не храните токены сброса пароля в чистом виде в БД. Вместо этого вы храните хэш токена. Это делается в целях безопасности. Даже если некий злоумышленик украдет БД, они не смогут сбросить пароли пользователей.

Алгоритм генерации токена сброса пароля реализован внутри метода 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;
}

Top