Atualiza estrutura MVC e integração com banco de dados

This commit is contained in:
2026-08-30 22:18:31 -03:00
parent 6231cc46a8
commit 966f6b7587
26 changed files with 3265 additions and 8 deletions
+515
View File
@@ -0,0 +1,515 @@
<?php
declare(strict_types=1);
class AuthController
{
/**
* Model responsável pelos usuários.
*/
private User $user;
/**
* Model responsável pelas tentativas
* de autenticação.
*/
private LoginAttempt $loginAttempt;
/**
* Número máximo de tentativas inválidas
* permitidas para uma mesma conta.
*/
private const MAX_TENTATIVAS_CONTA = 5;
/**
* Número máximo de tentativas inválidas
* permitidas para um mesmo endereço IP.
*/
private const MAX_TENTATIVAS_IP = 20;
/**
* Período utilizado para contabilizar
* as tentativas inválidas.
*/
private const JANELA_MINUTOS = 10;
/**
* Recebe os Models necessários para
* executar o processo de autenticação.
*/
public function __construct(
User $user,
LoginAttempt $loginAttempt
) {
$this->user = $user;
$this->loginAttempt = $loginAttempt;
}
/**
* Processa o acesso à página de login.
*
* GET:
* Exibe o formulário.
*
* POST:
* Processa a tentativa de autenticação.
*/
public function login(): void
{
if ($this->usuarioAutenticado()) {
header('Location: /dashboard.php');
exit;
}
/*
* Garante que a sessão possua
* um token CSRF.
*/
$this->gerarTokenCsrf();
$erro = '';
$email = trim(
(string)($_POST['email'] ?? '')
);
$senha = (string)(
$_POST['senha'] ?? ''
);
/*
* Obtém o endereço IP real do cliente.
*/
$ip = $this->obterIpCliente();
/*
* Processa somente requisições POST.
*/
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$erro = $this->processarLogin(
$email,
$senha,
$ip
);
}
/*
* Carrega a View responsável
* pela apresentação do formulário.
*/
require __DIR__ . '/../Views/auth/login.php';
}
/**
* Verifica se existe um usuário autenticado
* na sessão atual.
*/
private function usuarioAutenticado(): bool
{
return isset(
$_SESSION['usuario_id']
);
}
/**
* Cria o token CSRF caso ainda
* não exista na sessão.
*/
private function gerarTokenCsrf(): void
{
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(
random_bytes(32)
);
}
}
/**
* Processa todas as etapas de uma
* tentativa de autenticação.
*/
private function processarLogin(
string $email,
string $senha,
string $ip
): string {
/*
* Valida o token CSRF enviado
* pelo formulário.
*/
if (!$this->validarCsrf()) {
return 'E-mail ou senha inválidos.';
}
/*
* Valida os dados básicos enviados
* pelo usuário.
*/
if (
!$this->validarDadosLogin(
$email,
$senha
)
) {
return 'E-mail ou senha inválidos.';
}
/*
* Verifica o limite de tentativas
* para a conta.
*/
if ($this->contaBloqueada($email)) {
return 'E-mail ou senha inválidos.';
}
/*
* Verifica o limite de tentativas
* para o endereço IP.
*/
if ($this->ipBloqueado($ip)) {
return 'E-mail ou senha inválidos.';
}
/*
* Solicita ao Model o usuário
* correspondente ao e-mail informado.
*/
$usuario = $this->user->buscarPorEmail(
$email
);
/*
* Verifica as credenciais fornecidas.
*/
if (
$usuario !== null &&
(int)$usuario['ativo'] === 1 &&
password_verify(
$senha,
$usuario['senha']
)
) {
/*
* Registra a tentativa bem-sucedida.
*/
$this->loginAttempt->registrar(
$email,
$ip,
true
);
/*
* Cria a sessão autenticada.
*/
$this->criarSessaoAutenticada(
$usuario
);
/*
* Atualiza o hash da senha caso
* seja necessário utilizar parâmetros
* mais atuais.
*/
$this->atualizarHashSeNecessario(
$usuario,
$senha
);
header('Location: /dashboard.php');
exit;
}
/*
* Registra a tentativa inválida.
*/
$this->loginAttempt->registrar(
$email,
$ip,
false
);
/*
* Pequeno atraso para dificultar
* ataques automatizados.
*/
usleep(300000);
return 'E-mail ou senha inválidos.';
}
/**
* Valida o token CSRF enviado
* pelo formulário de login.
*/
private function validarCsrf(): bool
{
$token = (string)(
$_POST['csrf_token'] ?? ''
);
return (
$token !== '' &&
isset($_SESSION['csrf_token']) &&
hash_equals(
$_SESSION['csrf_token'],
$token
)
);
}
/**
* Valida os dados básicos utilizados
* durante a autenticação.
*/
private function validarDadosLogin(
string $email,
string $senha
): bool {
if (strlen($email) > 150) {
return false;
}
if (
$email === '' ||
$senha === ''
) {
return false;
}
if (
filter_var(
$email,
FILTER_VALIDATE_EMAIL
) === false
) {
return false;
}
return true;
}
/**
* Obtém o endereço IP real do cliente.
*
* Quando a requisição passa pelo Traefik,
* REMOTE_ADDR corresponde ao IP do Traefik.
*
* Nesse caso utilizamos X-Real-IP,
* conforme configurado no ambiente.
*/
private function obterIpCliente(): string
{
$remoteAddr =
$_SERVER['REMOTE_ADDR'] ?? '';
if ($remoteAddr === '172.18.0.3') {
$ip =
$_SERVER['HTTP_X_REAL_IP'] ?? '';
if (
filter_var(
$ip,
FILTER_VALIDATE_IP
) !== false
) {
return $ip;
}
}
if (
filter_var(
$remoteAddr,
FILTER_VALIDATE_IP
) !== false
) {
return $remoteAddr;
}
return '0.0.0.0';
}
/**
* Verifica se o endereço de e-mail
* atingiu o limite de tentativas inválidas.
*/
private function contaBloqueada(
string $email
): bool {
$quantidade =
$this->loginAttempt
->contarFalhasPorEmail(
$email,
self::JANELA_MINUTOS
);
return $quantidade
>= self::MAX_TENTATIVAS_CONTA;
}
/**
* Verifica se o endereço IP atingiu
* o limite de tentativas inválidas.
*/
private function ipBloqueado(
string $ip
): bool {
$quantidade =
$this->loginAttempt
->contarFalhasPorIp(
$ip,
self::JANELA_MINUTOS
);
return $quantidade
>= self::MAX_TENTATIVAS_IP;
}
/**
* Cria a sessão do usuário após
* uma autenticação bem-sucedida.
*
* A regeneração do ID impede ataques
* de session fixation.
*/
private function criarSessaoAutenticada(
array $usuario
): void {
session_regenerate_id(true);
/*
* Gera um novo token CSRF depois
* da autenticação.
*/
$_SESSION['csrf_token'] = bin2hex(
random_bytes(32)
);
$_SESSION['usuario_id'] =
$usuario['id'];
$_SESSION['usuario_nome'] =
$usuario['nome'];
$_SESSION['usuario_email'] =
$usuario['email'];
$_SESSION['usuario_perfil'] =
$usuario['perfil'];
/*
* Registra os tempos utilizados
* pelo controle de expiração da sessão.
*/
$_SESSION['login_time'] = time();
$_SESSION['last_activity'] = time();
}
/**
* Atualiza o hash da senha caso o algoritmo
* ou seus parâmetros tenham evoluído.
*
* A operação de banco é realizada pelo
* Model User.
*/
private function atualizarHashSeNecessario(
array $usuario,
string $senha
): void {
if (
!password_needs_rehash(
$usuario['senha'],
PASSWORD_DEFAULT
)
) {
return;
}
$novoHash = password_hash(
$senha,
PASSWORD_DEFAULT
);
$this->user->atualizarSenha(
(int)$usuario['id'],
$novoHash
);
}
/**
* Encerra a sessão do usuário.
*
* Remove os dados da sessão, invalida
* o cookie de sessão e destrói a sessão
* existente no servidor.
*/
public function logout(): void
{
/*
* Remove todos os dados armazenados
* na sessão atual.
*/
$_SESSION = [];
/*
* Remove o cookie de sessão
* do navegador.
*/
if (ini_get('session.use_cookies')) {
$params =
session_get_cookie_params();
setcookie(
session_name(),
'',
time() - 42000,
$params['path'],
$params['domain'],
(bool)$params['secure'],
(bool)$params['httponly']
);
}
/*
* Destrói a sessão no servidor.
*/
session_destroy();
/*
* Retorna o usuário para
* a tela de login.
*/
header('Location: /login.php');
exit;
}
}
+15
View File
@@ -0,0 +1,15 @@
cat > /home/opc/html/app/Controllers/Controller.php <<'EOF'
<?php
declare(strict_types=1);
/**
* Controller base da aplicação.
*
* Esta classe servirá como ponto de partida para
* os Controllers específicos do sistema.
*/
abstract class Controller
{
}
EOF
+47
View File
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
class DashboardController
{
/**
* Exibe a página principal do dashboard.
*
* A autenticação e o controle de expiração
* da sessão são realizados pelo security.php.
*/
public function index(): void
{
/*
* Verifica se existe um usuário autenticado.
*
* Caso contrário, redireciona para o login.
*/
if (!isset($_SESSION['usuario_id'])) {
header('Location: /login.php');
exit;
}
/*
* Dados básicos do usuário autenticado.
*
* Posteriormente esses dados poderão vir
* de um Model ou de um Service.
*/
$usuario = [
'id' => $_SESSION['usuario_id'],
'nome' => $_SESSION['usuario_nome'] ?? '',
'email' => $_SESSION['usuario_email'] ?? '',
'perfil' => $_SESSION['usuario_perfil'] ?? ''
];
/*
* Carrega a View do dashboard.
*
* A View recebe os dados através da variável
* $usuario e fica responsável somente pela
* apresentação das informações.
*/
require __DIR__ . '/../Views/dashboard/index.php';
}
}
+285
View File
@@ -0,0 +1,285 @@
<?php
declare(strict_types=1);
/**
* Controller responsável pelos mecanismos de segurança
* relacionados à sessão e às requisições HTTP.
*
* Responsabilidades:
*
* - configurar os parâmetros de segurança da sessão;
* - iniciar a sessão com segurança;
* - aplicar headers HTTP de segurança;
* - controlar o tempo de inatividade da sessão;
* - controlar o tempo máximo de uma sessão;
* - destruir sessões expiradas;
* - redirecionar o usuário quando a sessão expirar.
*/
class SecurityController
{
/**
* Tempo máximo permitido sem atividade do usuário.
*
* 1800 segundos = 30 minutos.
*/
private const SESSAO_INATIVIDADE_SEGUNDOS = 1800;
/**
* Tempo máximo absoluto de uma sessão.
*
* 28800 segundos = 8 horas.
*/
private const SESSAO_MAXIMA_SEGUNDOS = 28800;
/**
* Configura os parâmetros de segurança
* utilizados pelo cookie da sessão.
*
* Essas configurações devem ser aplicadas
* antes de session_start().
*/
public function configurarSessao(): void
{
ini_set(
'session.use_strict_mode',
'1'
);
ini_set(
'session.use_only_cookies',
'1'
);
ini_set(
'session.cookie_httponly',
'1'
);
ini_set(
'session.cookie_secure',
'1'
);
ini_set(
'session.cookie_samesite',
'Lax'
);
}
/**
* Inicia a sessão caso ainda não exista
* uma sessão ativa.
*/
public function iniciarSessao(): void
{
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}
}
/**
* Aplica headers HTTP destinados a reduzir
* diferentes tipos de ataques no navegador.
*
* Os headers utilizados são:
*
* - X-Content-Type-Options;
* - X-Frame-Options;
* - Referrer-Policy;
* - Permissions-Policy;
* - Content-Security-Policy;
* - Strict-Transport-Security.
*/
public function aplicarHeadersSeguranca(): void
{
header(
'X-Content-Type-Options: nosniff'
);
header(
'X-Frame-Options: DENY'
);
header(
'Referrer-Policy: strict-origin-when-cross-origin'
);
header(
'Permissions-Policy: camera=(), microphone=(), geolocation=()'
);
header(
"Content-Security-Policy: "
. "default-src 'self'; "
. "style-src 'self'; "
. "script-src 'self'; "
. "img-src 'self' data:; "
. "font-src 'self'; "
. "form-action 'self'; "
. "frame-ancestors 'none'; "
. "base-uri 'self'; "
. "object-src 'none'"
);
/*
* HSTS determina que o navegador deverá utilizar
* HTTPS durante o período definido.
*
* Esta aplicação já utiliza HTTPS através do Traefik.
*/
header(
'Strict-Transport-Security: max-age=31536000; includeSubDomains'
);
}
/**
* Verifica se a sessão autenticada ainda está dentro
* dos limites de tempo estabelecidos.
*
* São verificados dois limites:
*
* 1. tempo máximo de inatividade;
* 2. tempo máximo absoluto da sessão.
*/
public function verificarExpiracaoSessao(): void
{
/*
* Se não existir usuário autenticado,
* não há sessão protegida para verificar.
*/
if (!isset($_SESSION['usuario_id'])) {
return;
}
$agora = time();
$ultimoAcesso = (int) (
$_SESSION['last_activity'] ?? 0
);
$inicioSessao = (int) (
$_SESSION['login_time'] ?? 0
);
/*
* Verifica se o usuário ficou tempo demais
* sem realizar nenhuma atividade.
*/
if (
$ultimoAcesso > 0
&& ($agora - $ultimoAcesso)
> self::SESSAO_INATIVIDADE_SEGUNDOS
) {
$this->encerrarSessao(
'inatividade'
);
return;
}
/*
* Verifica se a sessão atingiu seu tempo
* máximo absoluto.
*/
if (
$inicioSessao > 0
&& ($agora - $inicioSessao)
> self::SESSAO_MAXIMA_SEGUNDOS
) {
$this->encerrarSessao(
'tempo_maximo'
);
return;
}
/*
* Registra a última atividade do usuário.
*/
$_SESSION['last_activity'] = $agora;
}
/**
* Encerra uma sessão expirada e redireciona
* o usuário para a tela de login.
*
* O motivo da expiração é enviado através
* de um parâmetro na URL.
*/
private function encerrarSessao(
string $motivo
): void {
/*
* Remove todos os dados armazenados
* na sessão atual.
*/
$_SESSION = [];
/*
* Remove o cookie da sessão do navegador.
*/
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(
session_name(),
'',
time() - 42000,
$params['path'],
$params['domain'],
(bool) $params['secure'],
(bool) $params['httponly']
);
}
/*
* Destrói a sessão no servidor.
*/
session_destroy();
/*
* Redireciona para o login informando
* que a sessão foi encerrada.
*/
header(
'Location: /login.php?expirada=1&motivo='
. urlencode($motivo)
);
exit;
}
/**
* Executa a sequência básica de segurança
* necessária para uma requisição da aplicação.
*
* Este método permite que as páginas utilizem
* apenas uma chamada para inicializar a segurança.
*/
public function iniciarSeguranca(): void
{
/*
* Configura o cookie antes de iniciar a sessão.
*/
$this->configurarSessao();
/*
* Inicia a sessão.
*/
$this->iniciarSessao();
/*
* Aplica os headers HTTP de segurança.
*/
$this->aplicarHeadersSeguranca();
/*
* Verifica se uma sessão autenticada
* expirou por tempo.
*/
$this->verificarExpiracaoSessao();
}
}
?>