Guía de Autenticación Segura en PHP: Control de Usuarios con Sesiones y Cookies
La seguridad en la gestión de usuarios es el pilar de cualquier aplicación web moderna. Implementar una autenticación segura en PHP requiere combinar el almacenamiento de contraseñas con password_hash(), la gestión de estado mediante $_SESSION y la configuración estricta de cookies para evitar accesos no autorizados.
Hasheado de Contraseñas con password_hash()
Hasheado de Contraseñas con password_hash()
Almacenar contraseñas en texto plano o usar algoritmos obsoletos como MD5 o SHA1 expone la base de datos a filtraciones. PHP ofrece las funciones nativas password_hash() y password_verify(), las cuales generan un salt aleatorio automático y emplean algoritmos robustos como Bcrypt y Argon2id.
Registro y Verificación de Credenciales
-
Procesamiento de contraseña en el registro:
PHP$password = $_POST['password']; $hashBD = password_hash($password, PASSWORD_DEFAULT); -
Validación durante el inicio de sesión:
PHPif (password_verify($passwordIngresada, $hashBD)) { // Credenciales válidas: Iniciar sesión }
Control de Estado y Seguridad con $_SESSION
Las variables de sesión permiten mantener la identidad del usuario a través de múltiples peticiones HTTP. Sin embargo, para prevenir ataques de fijación de sesión (Session Fixation) y secuestro de sesión (Session Hijacking), es indispensable configurar los parámetros de las cookies de sesión antes de iniciarlas.
Configuración Recomendada del Parámetro de Sesión
PHP
session_set_cookie_params([
'lifetime' => 0, // Expira al cerrar el navegador
'path' => '/',
'secure' => true, // Solo transmite cookies sobre HTTPS
'httponly' => true, // Impide el acceso a las cookies vía JavaScript (mitiga XSS)
'samesite' => 'Lax' // Previene ataques de falsificación de petición (CSRF)
]);
session_start();
session_regenerate_id(true); // Renueva la ID de sesión al autenticar
Persistencia de Usuarios con Cookies ("Recordarme")
Persistencia de Usuarios con Cookies ("Recordarme")
Guardar contraseñas o el ID directo del usuario en una cookie es un fallo grave de seguridad. El estándar de la industria exige el patrón de tokens selector/validador de un solo uso.
PHP
// Generar un par de tokens criptográficamente seguros
$selector = bin2hex(random_bytes(8));
$validator = random_bytes(32);
// Crear cookie persistente por 30 días
$tokenValue = $selector . ':' . bin2hex($validator);
$hashedValidator = hash('sha256', $validator);
setcookie(
'remember_me',
$tokenValue,
[
'expires' => time() + (86400 * 30),
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax'
]
);
// Guardar $selector y $hashedValidator en la base de datos asociados al usuario
Comparativa de Mecanismos de Seguridad
| Mecanismo | Función Principal | Riesgo de Seguridad que Previene |
password_hash() |
Cifrado unidireccional de clave | Filtración de credenciales en fuga de datos |
session_regenerate_id() |
Renovación del identificador de sesión | Fijación de sesión (Session Fixation) |
Cookie HttpOnly + Secure |
Restricción de acceso en el cliente | Robo de sesión mediante XSS e intercepción en red |
Flag SameSite=Lax |
Aislamiento de origen de cookies | Falsificación de Peticiones en Sitios Cruzados (CSRF) |
Control de Acceso en Páginas Protegidas
Para restringir secciones privadas de la aplicación, incluye un script de verificación en el encabezado de cada archivo protegido:
PHP
session_start();
// Verificar si el usuario está autenticado
if (!isset($_SESSION['logged_in']) || $_SESSION['logged_in'] !== true) {
header('Location: /login.php');
exit;
}
// Control de expiración por inactividad (30 minutos)
if (time() - $_SESSION['last_activity'] > 1800) {
session_unset();
session_destroy();
header('Location: /login.php?error=timeout');
exit;
}
$_SESSION['last_activity'] = time(); // Actualizar tiempo de última interacción
Buenas Prácticas para Proteger tu Sistema de Autenticación
- Sanitiza las entradas: Valida y limpia siempre los datos provenientes de formularios con
filter_input(). - Cierra sesiones inactivas: Implementa un tiempo de expiración automático tras 15 o 30 minutos de inactividad.
- Aplica tokens Anti-CSRF: Incluye un token único e impredecible en cada formulario
POSTpara confirmar el origen de la petición. - Destrucción completa al cerrar sesión: Al hacer logout, vacía
$_SESSION, elimina la cookie de sesión del navegador y destruye la sesión en el servidor consession_destroy().
Código completo archivos login.php y logout.php utilizando PDO y MySQL
Código completo archivos login.php y logout.php utilizando PDO y MySQL
config.php (Conexión PDO Segura)
PHP
<?php
declare(strict_types=1);
$host = '127.0.0.1';
$db = 'mi_base_de_datos';
$user = 'usuario_db';
$pass = 'contrasena_segura';
$charset = 'utf8mb4';
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO($dsn, $user, $pass, $options);
} catch (PDOException $e) {
// Registrar error en servidor y ocultar detalles sensibles al cliente
error_log('Error de conexión a la base de datos: ' . $e->getMessage());
exit('Error interno en el servidor.');
}
login.php (Procesamiento y Formulario)
PHP
<?php
declare(strict_types=1);
// Encabezados de seguridad HTTP
header("X-Frame-Options: DENY");
header("X-Content-Type-Options: nosniff");
header("Referrer-Policy: strict-origin-when-cross-origin");
// Configurar parámetros de cookie antes de iniciar la sesión
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => '',
'secure' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on',
'httponly' => true,
'samesite' => 'Lax'
]);
session_start();
// Redirigir al usuario si ya tiene una sesión activa
if (isset($_SESSION['logged_in']) && $_SESSION['logged_in'] === true) {
header('Location: dashboard.php');
exit;
}
require_once 'config.php';
// Generar token Anti-CSRF
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 1. Validar Token Anti-CSRF
$tokenSubmitted = $_POST['csrf_token'] ?? '';
if (!hash_equals($_SESSION['csrf_token'], $tokenSubmitted)) {
http_response_code(403);
die('Solicitud no válida (CSRF detectado).');
}
// 2. Validar y sanitizar datos recibidos
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
$password = $_POST['password'] ?? '';
if (!$email || empty($password)) {
$error = 'Por favor, ingresa un correo y contraseña válidos.';
} else {
// 3. Consulta preparada PDO contra inyección SQL
$stmt = $pdo->prepare('SELECT id, password_hash FROM users WHERE email = :email LIMIT 1');
$stmt->execute(['email' => $email]);
$user = $stmt->fetch();
// 4. Verificar la contraseña con el hash almacenado
if ($user && password_verify($password, $user['password_hash'])) {
// Renovar ID de sesión para prevenir Session Fixation
session_regenerate_id(true);
$_SESSION['user_id'] = $user['id'];
$_SESSION['logged_in'] = true;
$_SESSION['last_activity'] = time();
// Actualizar hash si la estrategia o costo del algoritmo ha cambiado
if (password_needs_rehash($user['password_hash'], PASSWORD_DEFAULT)) {
$newHash = password_hash($password, PASSWORD_DEFAULT);
$updateStmt = $pdo->prepare('UPDATE users SET password_hash = :hash WHERE id = :id');
$updateStmt->execute(['hash' => $newHash, 'id' => $user['id']]);
}
unset($_SESSION['csrf_token']);
header('Location: dashboard.php');
exit;
} else {
// Mensaje genérico para mitigar ataques de enumeración
$error = 'Credenciales incorrectas.';
}
}
}
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Iniciar Sesión</title>
</head>
<body>
<h2>Iniciar Sesión</h2>
<?php if (!empty($error)): ?>
<p style="color: red;"><?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?></p>
<?php endif; ?>
<form action="login.php" method="POST">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token'], ENT_QUOTES, 'UTF-8') ?>">
<div>
<label for="email">Correo electrónico:</label><br>
<input type="email" id="email" name="email" required autocomplete="email">
</div>
<br>
<div>
<label for="password">Contraseña:</label><br>
<input type="password" id="password" name="password" required autocomplete="current-password">
</div>
<br>
<button type="submit">Ingresar</button>
</form>
</body>
</html>
logout.php (Cierre Seguro)
PHP
<?php
declare(strict_types=1);
session_start();
// 1. Vaciar el arreglo de sesión
$_SESSION = [];
// 2. Eliminar la cookie de sesión en el cliente
if (ini_get("session.use_cookies")) {
$params = session_get_cookie_params();
setcookie(
session_name(),
'',
time() - 42000,
$params["path"],
$params["domain"],
$params["secure"],
$params["httponly"]
);
}
// 3. Destruir la sesión en el servidor
session_destroy();
// 4. Redirigir al inicio de sesión
header('Location: login.php');
exit;
Tabla MySQL (schema.sql)
SQL
CREATE TABLE IF NOT EXISTS `users` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`email` VARCHAR(255) NOT NULL UNIQUE,
`password_hash` VARCHAR(255) NOT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
Tutoriales Recomendados
Sigue explorando publicaciones de la misma categoría