Skip to content

Registro, login y logout ​

Sistema de autenticación completo con sesiones de PHP, contraseñas cifradas con password_hash, protección CSRF y regeneración del id de sesión.

Tabla necesaria ​

sql
CREATE TABLE usuarios (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  nombre VARCHAR(100) NOT NULL,
  email VARCHAR(150) NOT NULL UNIQUE,
  password_hash VARCHAR(255) NOT NULL,
  rol ENUM('admin','editor','cliente') NOT NULL DEFAULT 'cliente',
  activo TINYINT(1) NOT NULL DEFAULT 1,
  ultimo_login DATETIME NULL,
  creado_en TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Archivos base ​

helpers.php contiene utilidades que usarás en todos los demás snippets: escapar HTML, CSRF, redirecciones y mensajes flash.

php
<?php
// Inicia la sesión con cookies seguras (una sola vez)
if (session_status() === PHP_SESSION_NONE) {
    session_set_cookie_params([
        'lifetime' => 0,
        'path'     => '/',
        'secure'   => !empty($_SERVER['HTTPS']), // sólo HTTPS en producción
        'httponly' => true,                      // JS no puede leer la cookie
        'samesite' => 'Lax',
    ]);
    session_start();
}

/** Escapa texto para imprimirlo en HTML (evita XSS) */
function e(?string $texto): string
{
    return htmlspecialchars($texto ?? '', ENT_QUOTES, 'UTF-8');
}

function redirect(string $url): never
{
    header('Location: ' . $url);
    exit;
}

/** Token CSRF: uno por sesión */
function csrf_token(): string
{
    if (empty($_SESSION['csrf'])) {
        $_SESSION['csrf'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf'];
}

function csrf_field(): string
{
    return '<input type="hidden" name="csrf" value="' . csrf_token() . '">';
}

function csrf_check(): void
{
    $enviado = $_POST['csrf'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
    if (!hash_equals($_SESSION['csrf'] ?? '', $enviado)) {
        http_response_code(419);
        exit('La sesión expiró. Recarga la página e inténtalo de nuevo.');
    }
}

/** Mensajes de una sola lectura: flash('ok', 'Guardado') / flash('ok') */
function flash(string $clave, ?string $mensaje = null): ?string
{
    if ($mensaje !== null) {
        $_SESSION['flash'][$clave] = $mensaje;
        return null;
    }
    $valor = $_SESSION['flash'][$clave] ?? null;
    unset($_SESSION['flash'][$clave]);
    return $valor;
}

/** Recupera el valor anterior de un campo tras un error de validación */
function old(string $campo): string
{
    return e($_SESSION['old'][$campo] ?? '');
}
php
<?php
require_once __DIR__ . '/db.php';
require_once __DIR__ . '/helpers.php';

/** Devuelve el usuario logueado o null */
function usuario(): ?array
{
    static $usuario = false;
    if ($usuario === false) {
        $usuario = null;
        if (!empty($_SESSION['usuario_id'])) {
            $stmt = db()->prepare('SELECT id, nombre, email, rol FROM usuarios WHERE id = ? AND activo = 1');
            $stmt->execute([$_SESSION['usuario_id']]);
            $usuario = $stmt->fetch() ?: null;
        }
    }
    return $usuario;
}

function login(array $usuario): void
{
    session_regenerate_id(true); // evita fijación de sesión
    $_SESSION['usuario_id'] = $usuario['id'];
    db()->prepare('UPDATE usuarios SET ultimo_login = NOW() WHERE id = ?')->execute([$usuario['id']]);
}

function logout(): void
{
    $_SESSION = [];
    if (ini_get('session.use_cookies')) {
        $p = session_get_cookie_params();
        setcookie(session_name(), '', time() - 42000, $p['path'], $p['domain'], $p['secure'], $p['httponly']);
    }
    session_destroy();
}

/** Corta la ejecución si no hay sesión */
function requiere_login(): array
{
    $u = usuario();
    if (!$u) {
        flash('error', 'Inicia sesión para continuar.');
        redirect('login.php?volver=' . urlencode($_SERVER['REQUEST_URI']));
    }
    return $u;
}

Registro ​

php
<?php
require __DIR__ . '/includes/auth.php';

$errores = [];

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    csrf_check();

    $nombre = trim($_POST['nombre'] ?? '');
    $email  = strtolower(trim($_POST['email'] ?? ''));
    $pass   = $_POST['password'] ?? '';
    $pass2  = $_POST['password2'] ?? '';

    // Validaciones en servidor (las del navegador se pueden saltar)
    if (mb_strlen($nombre) < 2)                     $errores['nombre'] = 'Escribe tu nombre.';
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $errores['email'] = 'Correo no válido.';
    if (strlen($pass) < 8)                          $errores['password'] = 'Mínimo 8 caracteres.';
    if ($pass !== $pass2)                           $errores['password2'] = 'Las contraseñas no coinciden.';

    if (!$errores) {
        $stmt = db()->prepare('SELECT 1 FROM usuarios WHERE email = ?');
        $stmt->execute([$email]);
        if ($stmt->fetch()) {
            $errores['email'] = 'Ese correo ya está registrado.';
        }
    }

    if (!$errores) {
        $stmt = db()->prepare('INSERT INTO usuarios (nombre, email, password_hash) VALUES (?, ?, ?)');
        $stmt->execute([$nombre, $email, password_hash($pass, PASSWORD_DEFAULT)]);
        login(['id' => (int) db()->lastInsertId()]);
        flash('ok', '¡Bienvenido! Tu cuenta fue creada.');
        redirect('panel.php');
    }
}
?>
<!DOCTYPE html>
<html lang="es">
<head><meta charset="UTF-8"><title>Crear cuenta</title></head>
<body>
  <h1>Crear cuenta</h1>
  <form method="post" novalidate>
    <?= csrf_field() ?>
    <label>Nombre <input name="nombre" value="<?= e($_POST['nombre'] ?? '') ?>" required></label>
    <small><?= $errores['nombre'] ?? '' ?></small>

    <label>Correo <input type="email" name="email" value="<?= e($_POST['email'] ?? '') ?>" required></label>
    <small><?= $errores['email'] ?? '' ?></small>

    <label>Contraseña <input type="password" name="password" minlength="8" required></label>
    <small><?= $errores['password'] ?? '' ?></small>

    <label>Repite la contraseña <input type="password" name="password2" required></label>
    <small><?= $errores['password2'] ?? '' ?></small>

    <button>Registrarme</button>
  </form>
  <p>¿Ya tienes cuenta? <a href="login.php">Inicia sesión</a></p>
</body>
</html>

Login con límite de intentos ​

php
<?php
require __DIR__ . '/includes/auth.php';

if (usuario()) redirect('panel.php');

$error = flash('error');

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    csrf_check();

    // Freno simple contra fuerza bruta: 5 intentos cada 15 minutos por sesión
    $_SESSION['intentos'] = array_filter($_SESSION['intentos'] ?? [], fn($t) => $t > time() - 900);
    if (count($_SESSION['intentos']) >= 5) {
        $error = 'Demasiados intentos. Espera 15 minutos.';
    } else {
        $email = strtolower(trim($_POST['email'] ?? ''));
        $pass  = $_POST['password'] ?? '';

        $stmt = db()->prepare('SELECT id, password_hash FROM usuarios WHERE email = ? AND activo = 1');
        $stmt->execute([$email]);
        $u = $stmt->fetch();

        if ($u && password_verify($pass, $u['password_hash'])) {
            // Si PHP mejoró su algoritmo, actualiza el hash automáticamente
            if (password_needs_rehash($u['password_hash'], PASSWORD_DEFAULT)) {
                db()->prepare('UPDATE usuarios SET password_hash = ? WHERE id = ?')
                    ->execute([password_hash($pass, PASSWORD_DEFAULT), $u['id']]);
            }
            unset($_SESSION['intentos']);
            login($u);

            // Sólo redirige a rutas internas (evita "open redirect")
            $volver = $_GET['volver'] ?? 'panel.php';
            if (!str_starts_with($volver, '/') || str_starts_with($volver, '//')) $volver = 'panel.php';
            redirect($volver);
        }

        $_SESSION['intentos'][] = time();
        $error = 'Correo o contraseña incorrectos.'; // mensaje genérico a propósito
    }
}
?>
<!DOCTYPE html>
<html lang="es">
<head><meta charset="UTF-8"><title>Iniciar sesión</title></head>
<body>
  <h1>Iniciar sesión</h1>
  <?php if ($error): ?><p style="color:#b91c1c"><?= e($error) ?></p><?php endif; ?>
  <form method="post">
    <?= csrf_field() ?>
    <label>Correo <input type="email" name="email" required autofocus></label>
    <label>Contraseña <input type="password" name="password" required></label>
    <button>Entrar</button>
  </form>
  <p><a href="olvide.php">¿Olvidaste tu contraseña?</a> · <a href="registro.php">Crear cuenta</a></p>
</body>
</html>

Página protegida y logout ​

php
<?php
require __DIR__ . '/includes/auth.php';
$u = requiere_login();
$ok = flash('ok');
?>
<h1>Hola, <?= e($u['nombre']) ?></h1>
<?php if ($ok): ?><p><?= e($ok) ?></p><?php endif; ?>

<!-- El logout va por POST con CSRF para que nadie te cierre la sesión con un enlace -->
<form method="post" action="logout.php">
  <?= csrf_field() ?>
  <button>Cerrar sesión</button>
</form>
php
<?php
require __DIR__ . '/includes/auth.php';

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    exit;
}
csrf_check();
logout();
redirect('login.php');

Cómo integrarlo ​

  1. Asegúrate de tener la conexión PDO.
  2. Crea la tabla usuarios.
  3. Copia helpers.php y auth.php en includes/.
  4. En cualquier página privada, la primera línea es require 'includes/auth.php'; $u = requiere_login();.

Buena práctica

Nunca guardes la contraseña ni el hash en $_SESSION; guarda sólo el id. Así, si desactivas a un usuario en la base de datos, pierde el acceso en su siguiente petición.

Error común

Mostrar "el correo no existe" o "la contraseña es incorrecta" por separado. Eso le dice a un atacante qué correos están registrados. Usa siempre un mensaje genérico.

Academia Web — aprende, copia y construye.