Verificação de e-mail PHP

Resumo : neste tutorial, você aprenderá como verificar o endereço de e-mail da nova conta com segurança usando um link de ativação.

Introdução à verificação de e-mail PHP para novas contas

Nos tutoriais anteriores, você aprendeu como criar um formulário de registro que permite aos usuários registrarem-se em contas. E você também aprendeu como criar um formulário de login que permitirá aos usuários usar o nome de usuário e a senha para fazer login.

Quando os usuários se registram em novas contas, eles inserem seus endereços de e-mail. No entanto, os usuários podem inserir qualquer endereço de e-mail porque o sistema não verifica o e-mail.

Para verificar os endereços de e-mail dos usuários, você pode enviar um e-mail de verificação para esses endereços de e-mail e solicitar que os usuários abram seus e-mails e cliquem em um link de ativação.

Para fazer isso, siga as seguintes etapas quando os usuários registrarem contas:

  • Gere um código de ativação exclusivo e defina um prazo de validade, por exemplo, um dia.
  • Salve o registro do usuário no banco de dados e marque o status do usuário como inativo. Além disso, salve o hash do código de ativação e do prazo de validade.
  • Envie um email com o link de ativação para o endereço de email do usuário. O link de ativação conterá o endereço de e-mail e o código de ativação, por exemplo,https://app.com/activate.php?email=email&activation_code=abcd
  • Informe o usuário para ativar a conta por e-mail.

O hash do código de ativação garante que apenas o usuário que possui o endereço de e-mail possa ativar a conta, e mais ninguém, até mesmo o administrador, que possa acessar o banco de dados.

Se os usuários não tiverem a conta ativada, eles não poderão fazer login.

Quando os usuários clicam no link de ativação no e-mail, você precisa executar as seguintes etapas:

  • Limpe e valide o e-mail e o código de ativação.
  • Encontre o usuário inativo com o endereço de e-mail. Se não existir nenhum registro de usuário, redirecione para o formulário de registro.
  • Se existir um registro de usuário e o código de ativação expirou, exclua o registro do usuário do banco de dados e redirecione para o formulário de registro.
  • Caso contrário, combine o código de ativação com o hash do código de ativação armazenado no banco de dados. Se corresponderem, marque o registro do usuário como ativo e redirecione para a página de login.

Recrie a tabela de usuários

Primeiro, elimine a userstabela do authbanco de dados:

DROP TABLE users;Linguagem de código:  SQL (linguagem de consulta estruturada)  ( sql )

Segundo, crie a userstabela com as novas colunas active, activation_code, activation_at, activation_expiry:

CREATE TABLE users
(
    id                int auto_increment PRIMARY KEY,
    username          varchar(25)  NOT NULL,
    email             varchar(255) NOT NULL,
    password          varchar(255) NOT NULL,
    is_admin          tinyint(1)   NOT NULL DEFAULT 0,
    active            tinyint(1)            DEFAULT 0,
    activation_code   varchar(255) NOT NULL,
    activation_expiry datetime     NOT NULL,
    activated_at      datetime              DEFAULT NULL,
    created_at        timestamp    NOT NULL DEFAULT current_timestamp(),
    updated_at        datetime              DEFAULT current_timestamp() ON UPDATE current_timestamp()

);Linguagem de código:  SQL (linguagem de consulta estruturada)  ( sql )

A seguir explica-se o significado das novas colunas.

O valor padrão da activecoluna é 0. Isso significa que os usuários que se cadastram em contas, mas não verificaram seus endereços de e-mail, ficarão inativos por padrão.

A activation_codecoluna armazenará o hash do código de ativação. Seu comprimento deve ser suficiente para armazenar a string retornada pela password_hash()função.

É importante notar que o hash ficará truncado se a activation_codecoluna não tiver tamanho suficiente. Isso fará com que a password_verify()função não corresponda ao código de ativação com o hash.

A activation_expirycoluna armazena o prazo de validade para usar o código de ativação antes da expiração. O prazo de validade garante que o código de ativação não possa ser usado se o endereço de e-mail for comprometido após o prazo de validade.

A activated_atcoluna armazena a data e a hora em que os usuários ativam suas contas.

Estrutura do projeto

Vamos revisar a estrutura atual do projeto antes de adicionar as funções de verificação de e-mail:

├── config
|  ├── app.php
|  └── database.php
├── public
|  ├── index.php
|  ├── login.php
|  ├── logout.php
|  └── register.php
└── src
    ├── auth.php
    ├── bootstrap.php
    ├── inc
    |  ├── footer.php
    |  └── header.php
    ├── libs
    |  ├── connection.php
    |  ├── filter.php
    |  ├── flash.php
    |  ├── helpers.php
    |  ├── sanitization.php
    |  └── validation.php
    ├── login.php
    └── register.phpLinguagem de código:  PHP  ( php )

Modifique as funções no arquivo auth.php

O seguinte adiciona o código de ativação e o parâmetro de expiração à register_user()função. Por padrão, o prazo de validade é de um dia ( 1 * 24 * 60 * 60).

function register_user(string $email, string $username, string $password, string $activation_code, int $expiry = 1 * 24  * 60 * 60, bool $is_admin = false): bool
{
    $sql = 'INSERT INTO users(username, email, password, is_admin, activation_code, activation_expiry)
            VALUES(:username, :email, :password, :is_admin, :activation_code,:activation_expiry)';

    $statement = db()->prepare($sql);

    $statement->bindValue(':username', $username);
    $statement->bindValue(':email', $email);
    $statement->bindValue(':password', password_hash($password, PASSWORD_BCRYPT));
    $statement->bindValue(':is_admin', (int)$is_admin, PDO::PARAM_INT);
    $statement->bindValue(':activation_code', password_hash($activation_code, PASSWORD_DEFAULT));
    $statement->bindValue(':activation_expiry', date('Y-m-d H:i:s',  time() + $expiry));

    return $statement->execute();
}Linguagem de código:  PHP  ( php )

A register_user()função usa a password_hash()função para fazer hash do código de ativação.

A find_user_by_username()função inclui a activecoluna no resultado:

function find_user_by_username(string $username)
{
    $sql = 'SELECT username, password, active, email
            FROM users
            WHERE username=:username';

    $statement = db()->prepare($sql);
    $statement->bindValue(':username', $username);
    $statement->execute();

    return $statement->fetch(PDO::FETCH_ASSOC);
}Linguagem de código:  PHP  ( php )

O seguinte define uma nova função is_user_active()que retorna true se um usuário estiver ativo:

function is_user_active($user)
{
    return (int)$user['active'] === 1;
}Linguagem de código:  PHP  ( php )

A login()função deve permitir que apenas usuários ativos façam login:

function login(string $username, string $password): bool
{
    $user = find_user_by_username($username);

    if ($user && is_user_active($user) && password_verify($password, $user['password'])) {
        // prevent session fixation attack
        session_regenerate_id();

        // set username in the session
        $_SESSION['user_id'] = $user['id'];
        $_SESSION['username'] = $user['username'];

        return true;
    }

    return false;
}Linguagem de código:  PHP  ( php )

Defina funções que tratam da verificação de e-mail

Adicionaremos as funções que tratam da verificação de e-mail ao auth.phparquivo.

Primeiro, crie um novo arquivo app.phpna configpasta e defina as seguintes constantes:

<?php

const APP_URL = 'http://localhost/auth';
const SENDER_EMAIL_ADDRESS = '[email protected]';Linguagem de código:  PHP  ( php )

Usaremos essas constantes para enviar e-mails de ativação aos usuários. Para usar essas constantes, você precisa incluir o app.phparquivo no bootstrap.phparquivo:

<?php

session_start();
require_once __DIR__ . '/../config/app.php';
require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/libs/helpers.php';
require_once __DIR__ . '/libs/flash.php';
require_once __DIR__ . '/libs/sanitization.php';
require_once __DIR__ . '/libs/validation.php';
require_once __DIR__ . '/libs/filter.php';
require_once __DIR__ . '/libs/connection.php';
require_once __DIR__ . '/auth.php';Linguagem de código:  PHP  ( php )

Segundo, defina uma função que gere um código de ativação exclusivamente aleatório:

function generate_activation_code(): string
{
    return bin2hex(random_bytes(16));
}Linguagem de código:  PHP  ( php )

Terceiro, defina uma função que envie um e-mail de verificação com um link de ativação.

function send_activation_email(string $email, string $activation_code): void
{
    // create the activation link
    $activation_link = APP_URL . "/activate.php?email=$email&activation_code=$activation_code";

    // set email subject & body
    $subject = 'Please activate your account';
    $message = <<<MESSAGE
            Hi,
            Please click the following link to activate your account:
            $activation_link
            MESSAGE;
    // email header
    $header = "From:" . SENDER_EMAIL_ADDRESS;

    // send the email
    mail($email, $subject, nl2br($message), $header);

}Linguagem de código:  PHP  ( php )

Suponha que o URL do aplicativo seja http://localhost/auth, o URL de ativação será semelhante a este:

http://localhost/auth/[email protected]&activation_code=e01e5c9a028d58d888ff2555b971c882Linguagem de código:  PHP  ( php )

A send_activation_email()função usa a função integrada mail()para enviar e-mails.

Quarto, defina uma função que exclua um usuário por ID e status. Por padrão, ele exclui um usuário inativo por ID.

function delete_user_by_id(int $id, int $active = 0)
{
    $sql = 'DELETE FROM users
            WHERE id =:id and active=:active';

    $statement = db()->prepare($sql);
    $statement->bindValue(':id', $id, PDO::PARAM_INT);
    $statement->bindValue(':active', $active, PDO::PARAM_INT);

    return $statement->execute();
}Linguagem de código:  PHP  ( php )

Quinto, defina uma função que encontre um usuário não verificado por meio de um e-mail e código de ativação. Se o código de ativação expirou, a função também exclui o registro do usuário chamando a delete_user_by_id()função.

function find_unverified_user(string $activation_code, string $email)
{

    $sql = 'SELECT id, activation_code, activation_expiry < now() as expired
            FROM users
            WHERE active = 0 AND email=:email';

    $statement = db()->prepare($sql);

    $statement->bindValue(':email', $email);
    $statement->execute();

    $user = $statement->fetch(PDO::FETCH_ASSOC);

    if ($user) {
        // already expired, delete the in active user with expired activation code
        if ((int)$user['expired'] === 1) {
            delete_user_by_id($user['id']);
            return null;
        }
        // verify the password
        if (password_verify($activation_code, $user['activation_code'])) {
            return $user;
        }
    }

    return null;
}Linguagem de código:  PHP  ( php )

Sexto, defina uma nova activate_user()função que ative um usuário por um id:

function activate_user(int $user_id): bool
{
    $sql = 'UPDATE users
            SET active = 1,
                activated_at = CURRENT_TIMESTAMP
            WHERE id=:id';

    $statement = db()->prepare($sql);
    $statement->bindValue(':id', $user_id, PDO::PARAM_INT);

    return $statement->execute();
}Linguagem de código:  PHP  ( php )

Modifique a página Register.php

A src/register.phpnecessidade de incorporar a lógica para lidar com a lógica de verificação de e-mail.

<?php

if (is_user_logged_in()) {
    redirect_to('index.php');
}

$errors = [];
$inputs = [];

if (is_post_request()) {
    $fields = [
        'username' => 'string | required | alphanumeric | between: 3, 25 | unique: users, username',
        'email' => 'email | required | email | unique: users, email',
        'password' => 'string | required | secure',
        'password2' => 'string | required | same: password',
        'agree' => 'string | required'
    ];

    // custom messages
    $messages = [
        'password2' => [
            'required' => 'Please enter the password again',
            'same' => 'The password does not match'
        ],
        'agree' => [
            'required' => 'You need to agree to the term of services to register'
        ]
    ];

    [$inputs, $errors] = filter($_POST, $fields, $messages);

    if ($errors) {
        redirect_with('register.php', [
            'inputs' => escape_html($inputs),
            'errors' => $errors
        ]);
    }

    $activation_code = generate_activation_code();

    if (register_user($inputs['email'], $inputs['username'], $inputs['password'], $activation_code)) {

        // send the activation email
        send_activation_email($inputs['email'], $activation_code);

        redirect_with_message(
            'login.php',
            'Please check your email to activate your account before signing in'
        );
    }

} else if (is_get_request()) {
    [$errors, $inputs] = session_flash('errors', 'inputs');
}Linguagem de código:  PHP  ( php )

Como funciona.

Primeiro, gere um código de ativação:

$activation_code = generate_activation_code();Linguagem de código:  PHP  ( php )

Segundo, registre o usuário com o código de ativação:

register_user($inputs['email'], $inputs['username'], $inputs['password'], $activation_code)Linguagem de código:  PHP  ( php )

Terceiro, envie um email para o endereço de email do usuário chamando a send_activation_email()função:

send_activation_email($inputs['email'], $activation_code);Linguagem de código:  PHP  ( php )

Por fim, redirecione o usuário para a página de login e mostre uma mensagem flash solicitando que o usuário ative a conta por e-mail:

redirect_with_message(
    'login.php',
    'Please check your email to activate your account before signing in'
);Linguagem de código:  PHP  ( php )

Crie a página activate.php

Para permitir que os usuários ativem suas contas após o registro, você pode criar uma nova activate.phppágina na publicpasta e usar a seguinte página:

<?php

require __DIR__ . '/../src/bootstrap.php';

if (is_get_request()) {

    // sanitize the email & activation code
    [$inputs, $errors] = filter($_GET, [
        'email' => 'string | required | email',
        'activation_code' => 'string | required'
    ]);

    if (!$errors) {

        $user = find_unverified_user($inputs['activation_code'], $inputs['email']);

        // if user exists and activate the user successfully
        if ($user && activate_user($user['id'])) {
            redirect_with_message(
                'login.php',
                'You account has been activated successfully. Please login here.'
            );
        }
    }
}

// redirect to the register page in other cases
redirect_with_message(
    'register.php',
    'The activation link is not valid, please register again.',
    FLASH_ERROR
);Linguagem de código:  PHP  ( php )

Como funciona o activate.php.

Primeiro, higienize e valide o e-mail e o código de ativação:

[$inputs, $errors] = filter($_GET, [
    'email' => 'string | required | email',
    'activation_code' => 'string | required'
]);Linguagem de código:  PHP  ( php )

Em segundo lugar, encontre o usuário não verificado com base no e-mail e no código de verificação, se não houver erros de validação. O find_unverified_user()também excluirá o usuário não verificado se o tempo de expiração expirar.

$user = find_unverified_user($inputs['activation_code'], $inputs['email']);Linguagem de código:  PHP  ( php )

Terceiro, ative o usuário e redirecione para a página login.php:

if ($user && activate_user($user['id'])) {
    redirect_with_message(
        'login.php',
        'You account has been activated successfully. Please login here.'
    );
}Linguagem de código:  PHP  ( php )

Por fim, redirecione para se registration.phphouver um erro:

redirect_with_message(
    'register.php',
    'The activation link is not valid, please register again.',
    FLASH_ERROR
);Linguagem de código:  PHP  ( php )

Neste tutorial, você aprendeu como implementar a verificação de email para contas de usuário em PHP.

Deixe um comentário

O seu endereço de email não será publicado. Campos obrigatórios marcados com *