Sesiones y cookies en PHP
En esta lección
HTTP es un protocolo sin estado: cada petición llega al servidor como si fuera la primera, sin memoria de las anteriores. Si inicias sesión en una página y pasas a la siguiente, PHP no tiene forma de saber que eres tú… salvo que se lo recuerdes. Para eso existen las cookies y las sesiones. En esta lección aprenderás a usar las dos, a montar un login sencillo y seguro, a cerrarlo y a elegir cuál conviene en cada caso.
Cookies: datos que guarda el navegador
Una cookie es un pequeño texto con nombre y valor que el servidor pide al navegador que guarde. A partir de ese momento, el navegador la envía de vuelta en cada petición a esa web. Así el servidor “recuerda” cosas entre una página y otra.
1. Navegador ──GET /──▶ Servidor
2. Navegador ◀── Set-Cookie: idioma=gl ── Servidor
3. Navegador ──GET /horarios.php (Cookie: idioma=gl)──▶ Servidor
Las cookies se ven y se pueden borrar o modificar desde las DevTools del navegador (pestaña Application o Almacenamiento). Recuerda esto: el usuario controla sus cookies, así que nunca guardes en ellas nada que no pueda ver o cambiar, como “es administrador” o el precio de un producto.
Crear una cookie con setcookie
Desde PHP 7.3, setcookie() acepta un array de opciones, que es mucho más legible que la lista de siete parámetros que verás en código antiguo:
<?php
setcookie('idioma', 'gl', [
'expires' => time() + 60 * 60 * 24 * 365, // dentro de un año
'path' => '/', // válida en toda la web
'secure' => true, // solo por HTTPS
'httponly' => true, // JavaScript no puede leerla
'samesite' => 'Lax', // no se envía desde otras webs en peticiones POST
]);
| Opción | Qué hace |
|---|---|
expires | Momento en que caduca, en segundos desde 1970 (time() + segundos). Si no la pones, es una cookie de sesión y se borra al cerrar el navegador |
path | Parte de la web donde se envía. Con '/', en toda |
domain | Dominio. Si no la pones, solo el dominio actual |
secure | Solo se envía por HTTPS |
httponly | No es accesible desde document.cookie en JavaScript. Protege frente a robos con XSS |
samesite | Strict, Lax o None. Controla si se envía cuando la petición viene de otra web |
Nota: en tu servidor local (
http://localhost) no hay HTTPS. Con'secure' => trueel navegador no guardará la cookie. Para practicar, ponlo afalsey vuelve atrueal publicar.
Igual que header(), setcookie() envía una cabecera HTTP, así que debe ir antes de cualquier salida (un echo, HTML o espacios). Si no, verás el aviso headers already sent.
Leer y borrar cookies
Las cookies que envía el navegador están en el array $_COOKIE:
<?php
$idioma = $_COOKIE['idioma'] ?? 'es';
Un detalle que sorprende: la cookie que acabas de crear con setcookie() no aparece en $_COOKIE hasta la siguiente petición, porque $_COOKIE contiene lo que el navegador envió al pedir esta página, y en ese momento aún no la tenía.
Para borrar una cookie, vuelve a enviarla con una fecha de caducidad pasada y el mismo path:
<?php
setcookie('idioma', '', ['expires' => time() - 3600, 'path' => '/']);
Sesiones: datos que guarda el servidor
Una sesión guarda los datos en el servidor, no en el navegador. Al navegador solo se le da una cookie con un identificador aleatorio y largo (por defecto se llama PHPSESSID). En cada petición, PHP lee ese identificador, busca el archivo de sesión que le corresponde y lo carga en el array $_SESSION.
Navegador: Cookie PHPSESSID=k3j9a8... ──▶ Servidor: archivo sess_k3j9a8... = ['usuario' => 'marta', 'carrito' => [...]]
Como el usuario solo ve el identificador, no puede cambiar lo que hay dentro de la sesión. Por eso es el sitio adecuado para guardar quién ha iniciado sesión, el contenido de un carrito o mensajes entre páginas.
session_start y $_SESSION
Toda página que use la sesión debe llamar a session_start() al principio, antes de cualquier salida (también envía una cabecera). Después, $_SESSION se usa como un array normal y PHP lo guarda solo al terminar el script.
<?php
session_start();
$_SESSION['visitas'] = ($_SESSION['visitas'] ?? 0) + 1;
echo "Has cargado esta página {$_SESSION['visitas']} veces.";
// Has cargado esta página 1 veces. (y 2, 3... al recargar)
Para quitar un dato concreto, usa unset($_SESSION['visitas']).
Consejo: si casi todas tus páginas usan la sesión, pon
session_start()en un archivo común que incluyas al principio de cada una conrequire(lo viste en include y require).
Un login sencillo con password_verify
Vamos a montar un acceso a la zona de socios de un rocódromo. En una aplicación real los usuarios estarían en una base de datos (lo verás en MySQL con PDO); aquí usamos un array para centrarnos en la sesión.
Guardar contraseñas: nunca en texto plano
Las contraseñas no se guardan tal cual, sino como un hash: el resultado de una función matemática de un solo sentido. A partir del hash no se puede obtener la contraseña, pero sí comprobar si una contraseña coincide. PHP lo hace con dos funciones:
<?php
$hash = password_hash('Escalada2026', PASSWORD_DEFAULT);
echo $hash, "\n";
// $2y$12$Wd2... (cambia cada vez: incluye una "sal" aleatoria)
var_dump(password_verify('Escalada2026', $hash)); // bool(true)
var_dump(password_verify('escalada2026', $hash)); // bool(false)
password_hash() se usa al registrar al usuario y password_verify() al iniciar sesión. Verás en código antiguo md5() o sha1() para esto: son rapidísimas, y precisamente por eso un atacante puede probar miles de millones de contraseñas por segundo. Más detalles en Seguridad.
login.php
<?php
session_start();
// Usuarios de ejemplo: el hash se generó con password_hash()
$usuarios = [
'marta' => '$2y$12$DRZ70MIhTvpv12ioTpaxXOoLHSxe05/x2qdUkRXi4k/u.ouGsZlo2', // Escalada2026
];
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$usuario = trim($_POST['usuario'] ?? '');
$clave = $_POST['clave'] ?? '';
$hash = $usuarios[$usuario] ?? null;
if ($hash !== null && password_verify($clave, $hash)) {
session_regenerate_id(true); // identificador nuevo al iniciar sesión
$_SESSION['usuario'] = $usuario;
header('Location: socios.php', true, 303);
exit;
}
$error = 'Usuario o contraseña incorrectos.';
}
?>
<!DOCTYPE html>
<html lang="es">
<head><meta charset="utf-8"><title>Acceso socios</title></head>
<body>
<?php if ($error): ?><p><?= htmlspecialchars($error) ?></p><?php endif; ?>
<form method="post">
<label>Usuario <input name="usuario" autocomplete="username"></label>
<label>Contraseña <input type="password" name="clave" autocomplete="current-password"></label>
<button>Entrar</button>
</form>
</body>
</html>
Tres detalles de seguridad:
- El mensaje de error es el mismo si falla el usuario o la contraseña. Si dijeras “ese usuario no existe”, un atacante sabría qué nombres son válidos.
session_regenerate_id(true)cambia el identificador de sesión justo al iniciar sesión y borra el antiguo. Evita el ataque de fijación de sesión, en el que alguien consigue que la víctima use un identificador que él ya conoce y, cuando ella inicia sesión, entra con su cuenta.- Tras el login se redirige (patrón PRG, que viste en Formularios GET y POST).
Proteger una página: socios.php
Cualquier página privada empieza comprobando que hay alguien en la sesión. Si no, redirige al login:
<?php
session_start();
if (!isset($_SESSION['usuario'])) {
header('Location: login.php');
exit;
}
?>
<h1>Hola, <?= htmlspecialchars($_SESSION['usuario']) ?></h1>
<p>Tus bonos y reservas aparecerán aquí.</p>
<form method="post" action="logout.php"><button>Cerrar sesión</button></form>
El exit es imprescindible: sin él, PHP enviaría la redirección y también el contenido privado de la página. Un navegador obedece la redirección, pero alguien con curl vería todo.
Cerrar sesión
Cerrar la sesión bien son tres pasos: vaciar los datos, borrar la cookie del navegador y destruir el archivo del servidor.
<?php
// logout.php
session_start();
$_SESSION = [];
$p = session_get_cookie_params();
setcookie(session_name(), '', [
'expires' => time() - 3600,
'path' => $p['path'],
'domain' => $p['domain'],
'secure' => $p['secure'],
'httponly' => $p['httponly'],
'samesite' => $p['samesite'],
]);
session_destroy();
header('Location: login.php');
exit;
Fíjate en que el botón de cerrar sesión está en un formulario con POST, no en un enlace. Una acción que cambia algo debe ir por POST (lo verás en Seguridad al hablar de CSRF).
Configurar la cookie de sesión
La cookie PHPSESSID también puede llevar las opciones seguras que viste antes. Puedes indicarlas en el php.ini o, para tu aplicación, antes de session_start():
<?php
session_set_cookie_params([
'lifetime' => 0, // cookie de sesión: se borra al cerrar el navegador
'path' => '/',
'secure' => true, // pon false en http://localhost
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
Por defecto, PHP borra los datos de sesiones inactivas al cabo de unos 24 minutos (session.gc_maxlifetime = 1440 segundos). Si quieres cerrar tú la sesión tras un rato sin actividad, guarda la hora del último acceso y compárala:
<?php
session_start();
$limite = 30 * 60; // 30 minutos
if (isset($_SESSION['ultimo_acceso']) && time() - $_SESSION['ultimo_acceso'] > $limite) {
$_SESSION = [];
session_destroy();
header('Location: login.php?caducada=1');
exit;
}
$_SESSION['ultimo_acceso'] = time();
Mensajes flash
Un mensaje flash es un aviso que se guarda en la sesión, se muestra una sola vez en la siguiente página y se borra. Es el compañero perfecto del patrón PRG: procesas el formulario, guardas “Reserva confirmada” en la sesión, rediriges, y la página de destino lo muestra.
<?php
function flash(string $mensaje): void
{
$_SESSION['flash'] = $mensaje;
}
function leerFlash(): ?string
{
$mensaje = $_SESSION['flash'] ?? null;
unset($_SESSION['flash']);
return $mensaje;
}
<?php
// Al procesar el formulario
flash('Reserva confirmada para el sábado.');
header('Location: reservas.php', true, 303);
exit;
<?php // En reservas.php, después de session_start() ?>
<?php if ($mensaje = leerFlash()): ?>
<p class="aviso"><?= htmlspecialchars($mensaje) ?></p>
<?php endif; ?>
Si el usuario recarga reservas.php, el mensaje ya no aparece. Lo usarás en el CRUD con PDO.
¿Cookie o sesión?
| Cookie | Sesión | |
|---|---|---|
| Dónde se guardan los datos | En el navegador | En el servidor |
| El usuario puede verlos o cambiarlos | Sí | No (solo ve el identificador) |
| Duración | La que indiques (días, meses) | Hasta cerrar el navegador o inactividad |
| Tamaño | Unos 4 KB por cookie | Lo que necesites |
| Uso típico | Preferencias: idioma, tema oscuro, “recordar mi usuario” | Login, carrito, mensajes flash, token CSRF |
Regla práctica: si el dato es confidencial o importante, a la sesión. Si es una preferencia sin riesgo que debe durar semanas, a una cookie.
Cuidado: nunca hagas un “recuérdame” guardando el usuario o la contraseña en una cookie. Se hace con un token aleatorio largo guardado (con hash) en la base de datos, y es más delicado de lo que parece. Para empezar, deja que la sesión dure lo que dura el navegador.
Cookies y la ley
En España y en el resto de la Unión Europea, las cookies que no son técnicas (analítica, publicidad, redes sociales) necesitan el consentimiento previo del usuario, y la web debe explicar qué cookies usa. Las estrictamente necesarias para que la web funcione, como la cookie de sesión de un login o un carrito, normalmente no lo necesitan. Esto no es asesoramiento legal: si vas a publicar una web real, consulta la guía sobre el uso de cookies de la Agencia Española de Protección de Datos.
Errores frecuentes
- Olvidar
session_start():$_SESSIONestá vacío en esa página aunque en otra tenga datos. Va en todas las páginas que usan la sesión. - Llamar a
session_start()osetcookie()después de imprimir algo: headers already sent. Ponlos al principio, sin espacios antes de<?php. - Esperar ver la cookie recién creada en
$_COOKIE: aparece en la siguiente petición. - Proteger una página sin
exittras la redirección: el contenido privado se envía igualmente. - Guardar datos sensibles en cookies: el usuario puede leerlas y cambiarlas. Usa la sesión.
- No regenerar el identificador al iniciar sesión: te expones a la fijación de sesión.
session_regenerate_id(true). 'secure' => trueen localhost: la cookie no se guarda porque no hay HTTPS.
Resumen
| Tarea | Código |
|---|---|
| Crear una cookie | setcookie('tema', 'oscuro', ['expires' => time() + 86400, 'path' => '/', 'httponly' => true, 'samesite' => 'Lax']) |
| Leer una cookie | $_COOKIE['tema'] ?? 'claro' |
| Borrar una cookie | setcookie('tema', '', ['expires' => time() - 3600, 'path' => '/']) |
| Empezar o reanudar la sesión | session_start() |
| Guardar / leer en sesión | $_SESSION['usuario'] = 'marta' / $_SESSION['usuario'] ?? null |
| Hash de contraseña | password_hash($clave, PASSWORD_DEFAULT) |
| Comprobar contraseña | password_verify($clave, $hash) |
| Al iniciar sesión | session_regenerate_id(true) |
| Cerrar sesión | $_SESSION = [], borrar la cookie y session_destroy() |
En la siguiente lección aprenderás a leer y escribir archivos y a dejar que los usuarios suban los suyos: Ficheros y subidas.
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.