A free and open-source book on ZF3 for beginners

Translation into this language is not yet finished. You can help this project by translating the chapters and contributing your changes.

16.6. Agregar el Servicio UserManager

El UserController trabaja en paralelo con el servicio UserManager, que contiene toda la lógica de negocio relacionada con el manejo de usuario. El servicio permite al administrador crear y actualizar usuarios, cambiar y reiniciar sus contraseñas. Describimos algunas partes de él con más detalles omitiendo otras partes obvias (siempre podemos ver el código completo en el ejemplo User Demo).

16.6.1. Crear un nuevo usuario y guardar su contraseña encriptada

El método addUser del UserManager permite agregar un nuevo usuario. Su aspecto es este:

/**
 * This method adds a new user.
 */
public function addUser($data)
{
    // Do not allow several users with the same email address.
    if($this->checkUserExists($data['email'])) {
        throw new \Exception("User with email address " .
                    $data['$email'] . " already exists");
    }

    // Create new User entity.
    $user = new User();
    $user->setEmail($data['email']);
    $user->setFullName($data['full_name']);

    // Encrypt password and store the password in encrypted state.
    $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);

    // Add the entity to the entity manager.
    $this->entityManager->persist($user);

    // Apply changes to database.
    $this->entityManager->flush();

    return $user;
}

Como podemos ver con este método primero revisamos si otro usuario con la misma dirección de correo electrónico ya existe (linea 7) e impedimos su creación mediante el lanzamiento de una excepción.

Si el usuario, es decir el correo electrónico, no existe creamos una nueva entidad User (linea 13) y colocamos sus propiedades adecuadamente.

Lo que es interesante aquí es como guardamos la contraseña del usuario en la base de datos. Por razones de seguridad no guardamos la contraseña tal cual sino que calculamos un hash de ella con la clase Bcrypt que esta en Zend\Crypt (lines 18-19), un componente de Zend Framework.

Podemos instalar Zend\Crypt con el siguiente comando:

php composer.phar require zendframework/zend-crypt

El componente Zend\Crypt también necesita que tengamos instalada la extensión de PHP mcrypt.

El algoritmo Bcrypt es un algoritmo de hashing ampliamente usado y recomendado por la comunidad de seguridad para guardar la contraseña del usuario. Cifrar la contraseña con Bcrypt se considera hoy en día como seguro. Algunos desarrolladores aun cifran las contraseñas con MD5 o SHA1 con Sal, pero esto ya no se considera seguro (los hashes MD5 y SHA1 pueden ser descifrados).

16.6.2. Validar la contraseña encriptada

Cuando un usuario inicia sesión necesitamos revisar si el hash de la contraseña almacenado en la base de datos es el mismo que el hash generado con la contraseña introducida por el visitante. Podemos hacer esto con la ayuda del método verify() provisto por la clase Bcrypt, de la siguiente manera:

/**
 * Checks that the given password is correct.
 */
public function validatePassword($user, $password)
{
    $bcrypt = new Bcrypt();
    $passwordHash = $user->getPassword();

    if ($bcrypt->verify($password, $passwordHash)) {
        return true;
    }

    return false;
}

16.6.3. Crear el usuario administrador

El otro elemento importante que hay que notar en el UserManager es como creamos el usuarios Admin.

El usuario Admin es un usuario inicial que se crea automáticamente cuando no existen usuarios en la base de datos permitiendo que iniciemos sesión por primera vez.

/**
 * This method checks if at least one user presents, and if not, creates
 * 'Admin' user with email 'admin@example.com' and password '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();
    }
}

Le colocamos al usuario Admin el correo electrónico admin@example.com y la contraseña Secur1ty, así podemos iniciar sesión por primera vez con estas credenciales.

16.6.4. Reiniciar la contraseña

Algunos usuarios olvidan su contraseña. Si esto ocurre necesitamos permitir que el usuario reiniciar su contraseña de forma segura. El reinicio de la contraseña funciona de la siguiente manera:

Generalmente no guardamos el token «crudo» de reinicio de contraseña en la base de datos. En su lugar guardamos un hash del token. Esto se hace por razones de seguridad. Incluso si en un ataque se roba la base de datos ellos no serán capaces de reiniciar la contraseña de los usuarios.

El algoritmo que genera el token de reinicio de contraseña se implementa dentro del método generatePassworkResetToken() del UserManager. Para generar una cadena de caracteres aleatoria usamos la clase Rand provista por el componente Zend\Math.

/**
 * Generates a password reset token for the user. This token is then stored in database and
 * sent to the user's E-mail address. When the user clicks the link in E-mail message, he is
 * directed to the Set Password page.
 */
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);
}

Configurar un servidor de correo electrónico para nuestro sitio web consiste generalmente en contratar algún servicio de correo como SendGrid o Amazon SES.

La validación del token de reinicio de contraseña está implementado dentro del método validatePasswordResetToken(). Revisamos que el hash del token es el mismo que el guardado en base de datos y que el token no ha vencido (el token vence un día después de su creación).

/**
 * Checks whether the given password reset token is a valid one.
 */
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;
}

Finalmente setPasswordByToken() permite colocar una nueva contraseña para el usuario.

/**
 * This method sets new password by password reset token.
 */
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