PHP básico

Sesiones y una zona con contraseña

Lección 7 de 8 · 16 min

El servidor no se acuerda de ti

Escribes usuario y contraseña, entras, y en la página siguiente el sitio ya sabe quién eres. Parece obvio, y no lo es en absoluto: cada petición al servidor es independiente de la anterior.

El servidor no recuerda nada entre una página y otra. Atiende, responde y olvida. Si no se hace algo, la segunda página no tiene forma de saber que la primera te dejó entrar.

Cómo se resuelve: la sesión

  1. El servidor abre un expediente

    Al llamar a session_start(), PHP crea un espacio en el servidor para ese visitante y le pone un identificador largo y aleatorio.

  2. Le entrega una ficha al navegador

    Ese identificador -y solo eso- viaja al navegador en una cookie. Los datos se quedan en el servidor.

  3. El navegador la enseña cada vez

    En cada petición siguiente el navegador manda la cookie, PHP busca el expediente y te devuelve tus datos en $_SESSION.

  4. Y por eso hay que proteger la ficha

    Quien consiga ese identificador es tratado como tú. De ahí las precauciones que vienen a continuación, y de ahí que todo esto se sirva por HTTPS.

Guardar y leer un dato de la sesión
<?php
session_start();

$_SESSION['usuario_id'] = 42;
$_SESSION['nombre']     = 'Ana Gómez';

// En cualquier otra página del sitio:
session_start();
echo 'Hola, ' . htmlspecialchars($_SESSION['nombre'] ?? 'visitante');

session_start() va antes de imprimir nada. Tiene que enviar la cookie, y las cookies viajan en las cabeceras, que salen antes que el contenido.

Si ves «headers already sent», ya sabes de la lección 6 dónde mirar: un espacio o un salto de línea detrás de un ?> en algún archivo incluido.

La contraseña: no se guarda, se guarda su huella

Ahora la parte que hay que hacer bien a la primera. Una contraseña nunca se guarda tal como el usuario la escribió, y tampoco «cifrada» de forma que se pueda recuperar.

Se guarda un hash: un resultado del que no se puede volver atrás. Con él se puede comprobar si una contraseña coincide, pero no se puede leer cuál era. Si alguien se lleva tu base de datos, no se lleva las contraseñas de tus usuarios.

Registrar: guardar la huella
<?php
$clave = $_POST['clave'] ?? '';

$hash = password_hash($clave, PASSWORD_DEFAULT);

// Esto es lo que se guarda en la base de datos:
echo $hash;
Resultado
$2y$12$KIXQ1s8vN0kEr7uJ5H4Xh.ePq6r3yVb2mWc9tZaLdF8gNhYpQrS1u
Entrar: comprobar contra la huella
<?php
session_start();

$correo = trim($_POST['correo'] ?? '');
$clave  = $_POST['clave'] ?? '';

$usuario = buscarUsuarioPorCorreo($correo);   // lo veremos en la lección 8

if ($usuario !== null && password_verify($clave, $usuario['clave_hash'])) {
    session_regenerate_id(true);
    $_SESSION['usuario_id'] = $usuario['id'];
    header('Location: /panel.php');
    exit;
}

// Un solo mensaje para los dos casos, a propósito.
$error = 'Correo o contraseña incorrectos.';

Las cuatro decisiones de ese archivo

password_verify, nunca ===

El hash lleva dentro el algoritmo, el coste y la sal, así que password_verify() sabe repetir el cálculo y comparar.

Compararlo tú con === no funciona: dos hashes de la misma contraseña son distintos, y esa es justamente la idea.

session_regenerate_id(true)

Cambia el identificador de sesión en el momento de entrar.

Si alguien logró fijarle a la víctima un identificador conocido antes del login, al cambiarlo se queda con uno que ya no sirve.

El mismo mensaje para los dos fallos

«El correo no existe» le confirma a quien prueba que el otro correo existe. Así se hace una lista de usuarios válidos.

Un solo mensaje para las dos situaciones no da esa pista.

exit después de header

header('Location: ...') pide al navegador que vaya a otro sitio, pero no detiene el programa.

Sin el exit, el código de abajo se sigue ejecutando y puede llegar a enviar contenido de la página privada.

Cerrar la zona privada: cada página, por su cuenta

Aquí está el error que más se repite, y lo comete gente con experiencia: quitar el enlace del menú y dar la página por protegida.

A una dirección se llega escribiéndola. Y también aparece en el historial, en un correo reenviado o en el buscador. El menú no es una puerta: es una sugerencia.

partes/solo-usuarios.php
<?php
session_start();

if (!isset($_SESSION['usuario_id'])) {
    header('Location: /entrar.php');
    exit;
}
Y como PRIMERA línea de cada página privada
<?php
require __DIR__ . '/partes/solo-usuarios.php';

// A partir de aquí, con la seguridad de que hay alguien identificado.
$titulo = 'Panel';
require __DIR__ . '/partes/cabecera.php';

La comprobación va arriba del todo, antes de imprimir nada. Si se hace a mitad del archivo, la parte de arriba de la página privada ya se envió al visitante antes de la redirección.

Y ese require va en todas las páginas privadas. La que se olvide queda abierta, y desde fuera se ve exactamente igual que las demás.

Salir

Cerrar sesión no es borrar una variable: hay que vaciar el expediente y tirar la ficha.

salir.php
<?php
session_start();

$_SESSION = [];
session_destroy();

header('Location: /entrar.php');
exit;

Preguntas que llegan justo aquí

¿Y si me olvido la contraseña? ¿Cómo se la mando?

No se la mandas: no la tienes, y eso es exactamente lo que se buscaba.

Lo que se hace es enviar un enlace con un código de un solo uso y con caducidad, que permite escribir una nueva. Un sitio que te reenvía tu contraseña por correo te está diciendo que la guarda legible.

Vi ejemplos con md5() y sha1()

Son de hace quince años y hoy son inseguros para contraseñas: están diseñados para ser rápidos, y eso es justo lo que ayuda a quien prueba millones por segundo.

password_hash() usa un algoritmo deliberadamente lento y con un coste ajustable. Desde PHP 8.4 ese coste por defecto subió de 10 a 12, precisamente porque el hardware mejora.

¿Cuánto dura una sesión?

Depende de la configuración del servidor -del orden de minutos de inactividad- y por defecto la cookie se borra al cerrar el navegador.

En una aplicación con datos sensibles, además, conviene cerrarla por inactividad: guardar en la sesión la hora del último movimiento y expulsar pasado un tiempo.

¿Qué exijo como contraseña?

Longitud, sobre todo. Las recomendaciones actuales van hacia frases largas y en contra de las reglas de composición -una mayúscula, un número, un símbolo-, que empujan a la gente hacia patrones predecibles como Clave2026*.

Lo que sí conviene es rechazar las contraseñas que ya aparecen en las filtraciones conocidas.

Compruébalo tú mismo

Quitas del menú el enlace a /panel.php para que solo lo vean los administradores. ¿Está protegida?

¿Por qué no se compara la contraseña con $hash === password_hash($clave, PASSWORD_DEFAULT)?

Tu login responde «ese correo no está registrado» cuando el correo no existe. ¿Qué problema tiene?

Lo que sigue

Falta una pieza: en esta lección apareció buscarUsuarioPorCorreo() como si existiera. En la última lección la escribes de verdad, conectando con una base de datos, y montas con todo lo del curso un proyecto que se puede publicar.

Siguiente: Base de datos con PDO y proyecto final

Continuar ← Armar el sitio por partes