Resumo : neste tutorial, você aprenderá a implementar com segurança o recurso lembre-me em PHP.
Introdução ao recurso Lembre-me do PHP
Quando os usuários fazem login em um aplicativo da Web e fecham os navegadores, os cookies de sessão associados aos logins expiram imediatamente. Isso significa que se os usuários acessarem a aplicação web posteriormente, eles precisarão fazer login novamente.
O recurso lembrar de mim permite que os usuários salvem seus logins por algum tempo, mesmo após fecharem os navegadores. Para implementar o recurso Lembre-me, você usará cookies com prazos de validade futuros.
A maneira comum, mas insegura
A maneira insegura de implementar o lembre-me é adicionar um ID de usuário ao cookie com um prazo de validade:
user_id=120
Linguagem de código: PHP ( php )
Quando os usuários acessam o aplicativo da web, você verifica se o ID do usuário no cookie é válido antes de fazer login automaticamente.
Esta abordagem ingênua depende apenas de cookies, que não são seguros pelos seguintes motivos:
- Primeiro, os usuários podem alterar o ID para outro para fazer login como outro usuário.
- Segundo, o ID do usuário pode revelar o número de usuários no sistema.
Uma abordagem mais segura
Uma maneira mais segura de implementar o recurso Lembre-me é armazenar um token aleatório em vez de um ID de usuário nos cookies e no servidor de banco de dados.
O valor nos cookies ficará assim:
remember_me=6179f9c66a9d007e689c7809b5d8320a6692787773488f12a4330cd5ffd25d91
Linguagem de código: PHP ( php )
E aqui está uma tabela de banco de dados que armazena os tokens:
CREATE TABLE user_tokens
(
id INT AUTO_INCREMENT PRIMARY KEY,
token VARCHAR(255) NOT NULL,
expiry DATETIME NOT NULL,
user_id INT NOT NULL,
CONSTRAINT fk_user_id
FOREIGN KEY (user_id)
REFERENCES users (id) ON DELETE CASCADE
);
Linguagem de código: SQL (linguagem de consulta estruturada) ( sql )
Quando os usuários acessam o aplicativo web, você combina os tokens dos cookies com aqueles armazenados no banco de dados. Além disso, você pode verificar o tempo de expiração do token. Se os tokens corresponderem e não tiverem expirado, você poderá obter o ID do usuário associado ao token e conectar o usuário automaticamente.
A consulta para correspondência do token ficará assim:
SELECT user_id
FROM user_tokens
WHERE token = '6179f9c66a9d007e689c7809b5d8320a6692787773488f12a4330cd5ffd25d91' and
expiry > NOW()
Linguagem de código: PHP ( php )
Essa abordagem resolve dois problemas acima:
- Primeiro, o token é mais difícil de adivinhar.
- Segundo, o token não revela o número de usuários.
No entanto, esta abordagem expõe outro problema de segurança conhecido como ataque de temporização.
Quando o banco de dados compara o token do cookie com o token armazenado no banco de dados, ele retorna os diferentes tempos de comparação de acordo com o quanto dois tokens são semelhantes.
Por exemplo, se você tiver o seguinte token armazenado no cookie:
6179f9c66a9d007e689c7809b5d8320a6692787773488f12a4330cd5ffd25d91
Linguagem de código: PHP ( php )
E o seguinte token no banco de dados:
6f9a1ef3020bb8351456cd65176e1e62ceeefcdca0a750201886a230f8736cad
Linguagem de código: PHP ( php )
Ao comparar esses tokens, o banco de dados compara cada caractere nos tokens e interrompe a correspondência quando encontra uma incompatibilidade. Neste exemplo, o banco de dados para no segundo caractere:
No entanto, ao comparar o seguinte par de tokens:
6179f9c66a9d007e689c7809b5d8320a6692787773488f12a4330cd5ffd25d91
6179f9c66a9d007e689c7809b5d8320a6692787773488f12a4330cd5ffd25d92
Linguagem de código: PHP ( php )
O banco de dados para de corresponder após comparar o penúltimo caractere.
O tempo de comparação no segundo exemplo será sempre maior que o segundo porque o banco de dados precisa comparar mais caracteres.
Ao testar tokens diferentes, você pode obter tempos de resposta diferentes. Em outras palavras, o tempo vazou. Para evitar vazamentos de tempo, a função de comparação precisa retornar um tempo constante, independentemente dos tokens.
Evite ataques de temporização
O seguinte mostra como prevenir o ataque de temporização proposto por PIE . Nesta abordagem, em vez de armazenar um único token no cookie, você armazena um par de tokens: selector
e validator
com o formato: selector:validator
.
O selector
serve para selecionar o validator
armazenado no banco de dados. No banco de dados, você armazena o hash do selector
e do validator
:
CREATE TABLE user_tokens
(
id INT AUTO_INCREMENT PRIMARY KEY,
selector VARCHAR(255) NOT NULL,
hashed_validator VARCHAR(255) NOT NULL,
user_id INT NOT NULL,
expiry DATETIME NOT NULL,
CONSTRAINT fk_user_id
FOREIGN KEY (user_id)
REFERENCES users (id) ON DELETE CASCADE
);
Linguagem de código: PHP ( php )
Para fazer o hash do validador, você usa a função password_hash().
Para obter um ID de usuário, você combina o selector
do cookie com o selector
do banco de dados:
SELECT id, selector, hashed_validator, user_id, expiry
FROM user_tokens
WHERE selector = :selector
Linguagem de código: PHP ( php )
Se a consulta retornar uma linha, você poderá combinar validator
from the cookie com the hashed_validator
usando a password_verify()
função.
Se os validadores corresponderem, você poderá logar o usuário user_id
automaticamente.
A seção a seguir aprimorará o sistema de login adicionando o recurso Lembre-me usando a terceira abordagem.
Crie uma tabela user_tokens para armazenar os tokens
A instrução a seguir cria uma user_tokens
tabela que armazena o selector
, hash validator
, expiry
e o ID do usuário.
CREATE TABLE user_tokens
(
id INT AUTO_INCREMENT PRIMARY KEY,
selector VARCHAR(255) NOT NULL,
hashed_validator VARCHAR(255) NOT NULL,
user_id INT NOT NULL,
expiry DATETIME NOT NULL,
CONSTRAINT fk_user_id
FOREIGN KEY (user_id)
REFERENCES users (id) ON DELETE CASCADE
);
Linguagem de código: PHP ( php )
Adicione a caixa de seleção lembrar de mim ao formulário de login
Primeiro, adicione uma caixa de seleção Lembre-me ao formulário de login no public/login.php
arquivo:
<?php
require __DIR__ . '/../src/bootstrap.php';
require __DIR__ . '/../src/login.php';
?>
<?php view('header', ['title' => 'Login']) ?>
<?php if (isset($errors['login'])) : ?>
<div class="alert alert-error">
<?= $errors['login'] ?>
</div>
<?php endif ?>
<form action="login.php" method="post">
<h1>Login</h1>
<div>
<label for="username">Username:</label>
<input type="text" name="username" id="username" value="<?= $inputs['username'] ?? '' ?>">
<small><?= $errors['username'] ?? '' ?></small>
</div>
<div>
<label for="password">Password:</label>
<input type="password" name="password" id="password">
<small><?= $errors['password'] ?? '' ?></small>
</div>
<div>
<label for="remember_me">
<input type="checkbox" name="remember_me" id="remember_me"
value="checked" <?= $inputs['remember_me'] ?? '' ?> />
Remember Me
</label>
<small><?= $errors['agree'] ?? '' ?></small>
</div>
<section>
<button type="submit">Login</button>
<a href="register.php">Register</a>
</section>
</form>
<?php view('footer') ?>
Linguagem de código: PHP ( php )
Em segundo lugar, adicione o código para lidar com a caixa de seleção lembrar de mim ao src/login.php
arquivo:
<?php
if (is_user_logged_in()) {
redirect_to('index.php');
}
$inputs = [];
$errors = [];
if (is_post_request()) {
[$inputs, $errors] = filter($_POST, [
'username' => 'string | required',
'password' => 'string | required',
'remember_me' => 'string'
]);
if ($errors) {
redirect_with('login.php', ['errors' => $errors, 'inputs' => $inputs]);
}
// if login fails
if (!login($inputs['username'], $inputs['password'], isset($inputs['remember_me']))) {
$errors['login'] = 'Invalid username or password';
redirect_with('login.php', [
'errors' => $errors,
'inputs' => $inputs
]);
}
// login successfully
redirect_to('index.php');
} else if (is_get_request()) {
[$errors, $inputs] = session_flash('errors', 'inputs');
}
Linguagem de código: PHP ( php )
No src/login.php
, adicione a caixa de seleção lembrar de mim à chamada da função filter():
[$inputs, $errors] = filter($_POST, [
'username' => 'string | required',
'password' => 'string | required',
'remember_me' => 'string'
]);
Linguagem de código: PHP ( php )
Além disso, adicione o terceiro parâmetro à função login():
login($inputs['username'], $inputs['password'], isset($inputs['remember_me'])
Linguagem de código: PHP ( php )
Voltaremos para aprimorar a login()
função mais tarde.
Definir funções para lidar com o recurso Lembre-me
Primeiro, crie o remember.php
arquivo na src
pasta.
Segundo, defina as seguintes novas funções para manipular os tokens no remember.php
arquivo:
Gerar tokens
O seguinte define como generate_tokens()
gerar um par de tokens aleatórios chamados seletor e validador:
function generate_tokens(): array
{
$selector = bin2hex(random_bytes(16));
$validator = bin2hex(random_bytes(32));
return [$selector, $validator, $selector . ':' . $validator];
}
Linguagem de código: PHP ( php )
A generate_tokens()
função retorna uma matriz de três elementos: selector
, valdiator
e selector:validator
.
Analise o token
A parse_token()
função a seguir divide o token armazenado no cookie em selector
e validator
:
function parse_token(string $token): ?array
{
$parts = explode(':', $token);
if ($parts && count($parts) == 2) {
return [$parts[0], $parts[1]];
}
return null;
}
Linguagem de código: PHP ( php )
Insira um novo token de usuário
A insert_user_tokens()
função a seguir adiciona uma nova linha à user_tokens
tabela:
function insert_user_token(int $user_id, string $selector, string $hashed_validator, string $expiry): bool
{
$sql = 'INSERT INTO user_tokens(user_id, selector, hashed_validator, expiry)
VALUES(:user_id, :selector, :hashed_validator, :expiry)';
$statement = db()->prepare($sql);
$statement->bindValue(':user_id', $user_id);
$statement->bindValue(':selector', $selector);
$statement->bindValue(':hashed_validator', $hashed_validator);
$statement->bindValue(':expiry', $expiry);
return $statement->execute();
}
Linguagem de código: PHP ( php )
Encontre o token por um seletor
A find_user_token_by_selector()
função a seguir encontra uma linha na user_tokens
tabela por um seletor. Ele só retorna o seletor de correspondência se o token não tiver expirado, comparando a expiração com o horário atual.
function find_user_token_by_selector(string $selector)
{
$sql = 'SELECT id, selector, hashed_validator, user_id, expiry
FROM user_tokens
WHERE selector = :selector AND
expiry >= now()
LIMIT 1';
$statement = db()->prepare($sql);
$statement->bindValue(':selector', $selector);
$statement->execute();
return $statement->fetch(PDO::FETCH_ASSOC);
}
Linguagem de código: PHP ( php )
Excluir um token de usuário
A delete_user_token()
função a seguir exclui todos os tokens associados a um usuário:
function delete_user_token(int $user_id): bool
{
$sql = 'DELETE FROM user_tokens WHERE user_id = :user_id';
$statement = db()->prepare($sql);
$statement->bindValue(':user_id', $user_id);
return $statement->execute();
}
Linguagem de código: PHP ( php )
Encontre um usuário por um token
A find_user_by_token()
função a seguir retorna user_id
e username
por um token.
function find_user_by_token(string $token)
{
$tokens = parse_token($token);
if (!$tokens) {
return null;
}
$sql = 'SELECT users.id, username
FROM users
INNER JOIN user_tokens ON user_id = users.id
WHERE selector = :selector AND
expiry > now()
LIMIT 1';
$statement = db()->prepare($sql);
$statement->bindValue(':selector', $tokens[0]);
$statement->execute();
return $statement->fetch(PDO::FETCH_ASSOC);
}
Linguagem de código: PHP ( php )
Verifique se um token é válido
A toke_is_valid()
função a seguir analisa o token armazenado no cookie ( selector:validator
) e retorna true
se o token é válido e não expirou:
function token_is_valid(string $token): bool { // analisa o token para obter o seletor e o validador [$selector, $validator] = parse_token($token);
$tokens = find_user_token_by_selector($selector);
if (!$tokens) {
return false;
}
return password_verify($validator, $tokens['hashed_validator']);
Linguagem de código: PHP ( php )
Modificando a função no auth.php
A seguir descrevemos as alterações nas funções no auth.php
arquivo:
A função login()
O seguinte adiciona o terceiro parâmetro $remember
à login()
função:
function login(string $username, string $password, bool $remember = false): bool
{
$user = find_user_by_username($username);
// if user found, check the password
if ($user && is_user_active($user) && password_verify($password, $user['password'])) {
log_user_in($user);
if ($remember) {
remember_me($user['id']);
}
return true;
}
return false;
}
Linguagem de código: PHP ( php )
Se $remember
for true
, chame a remember_me()
função.
A função log_user_in()
A função log_user_in() registra um usuário:
/**
* log a user in
* @param array $user
* @return bool
*/
function log_user_in(array $user): bool
{
// prevent session fixation attack
if (session_regenerate_id()) {
// set username & id in the session
$_SESSION['username'] = $user['username'];
$_SESSION['user_id'] = $user['id'];
return true;
}
return false;
}
Linguagem de código: PHP ( php )
A função lembre-me()
O seguinte define a remember_me()
função:
function remember_me(int $user_id, int $day = 30)
{
[$selector, $validator, $token] = generate_tokens();
// remove all existing token associated with the user id
delete_user_token($user_id);
// set expiration date
$expired_seconds = time() + 60 * 60 * 24 * $day;
// insert a token to the database
$hash_validator = password_hash($validator, PASSWORD_DEFAULT);
$expiry = date('Y-m-d H:i:s', $expired_seconds);
if (insert_user_token($user_id, $selector, $hash_validator, $expiry)) {
setcookie('remember_me', $token, $expired_seconds);
}
}
Linguagem de código: PHP ( php )
A remember_me()
função salva o login de um usuário por um número especificado de dias. Por padrão, ele lembra o login por 30 dias.
A remember_me()
função faz o seguinte:
- Primeiro, gere
selector
,validator
e token (selector:validator
) - Segundo, insira uma nova linha na
user_tokens
tabela. - Terceiro, defina um cookie com o prazo de validade especificado.
A função logout()
Caso o usuário efetue logout, além de deletar a sessão, é necessário deletar os registros da user_tokens
tabela e remover o remember_me
cookie:
function logout(): void
{
if (is_user_logged_in()) {
// delete the user token
delete_user_token($_SESSION['user_id']);
// delete session
unset($_SESSION['username'], $_SESSION['user_id`']);
// remove the remember_me cookie
if (isset($_COOKIE['remember_me'])) {
unset($_COOKIE['remember_me']);
setcookie('remember_user', null, -1);
}
// remove all session data
session_destroy();
// redirect to the login page
redirect_to('login.php');
}
}
Linguagem de código: PHP ( php )
A função is_user_logged_in()
A is_user_logged_in()
função a seguir verifica se o usuário está logado no momento:
function is_user_logged_in(): bool
{
// check the session
if (isset($_SESSION['username'])) {
return true;
}
// check the remember_me in cookie
$token = filter_input(INPUT_COOKIE, 'remember_me', FILTER_SANITIZE_STRING);
if ($token && token_is_valid($token)) {
$user = find_user_by_token($token);
if ($user) {
return log_user_in($user);
}
}
return false;
}
Linguagem de código: PHP ( php )
Como funciona
- Primeiro, a função retorna
true
se a sessão tiver a chaveusername
. - Em seguida, verifique o token nos cookies e faça login do usuário se o token for válido.
Resumo
- O recurso lembrar de mim salva o login por algum tempo, mesmo depois que os navegadores da web são fechados.
- Use cookies para implementar o recurso Lembre-me.