Saltar al contenido
sesiones-y-cookies.php · devschool

Sesiones y cookies en PHP

Lección 16 de 26 · 12 min de lectura · Actualizado el

En esta lección
  1. Cookies: datos que guarda el navegador
  2. Sesiones: datos que guarda el servidor
  3. Un login sencillo con password_verify
  4. Cerrar sesión
  5. Configurar la cookie de sesión
  6. Mensajes flash
  7. ¿Cookie o sesión?
  8. Cookies y la ley
  9. Errores frecuentes
  10. Resumen

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.

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ónQué hace
expiresMomento 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
pathParte de la web donde se envía. Con '/', en toda
domainDominio. Si no la pones, solo el dominio actual
secureSolo se envía por HTTPS
httponlyNo es accesible desde document.cookie en JavaScript. Protege frente a robos con XSS
samesiteStrict, 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' => true el navegador no guardará la cookie. Para practicar, ponlo a false y vuelve a true al 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 con require (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).

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.

CookieSesión
Dónde se guardan los datosEn el navegadorEn el servidor
El usuario puede verlos o cambiarlosSíNo (solo ve el identificador)
DuraciónLa que indiques (días, meses)Hasta cerrar el navegador o inactividad
TamañoUnos 4 KB por cookieLo que necesites
Uso típicoPreferencias: 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(): $_SESSION está 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() o setcookie() 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 exit tras 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' => true en localhost: la cookie no se guarda porque no hay HTTPS.

Resumen

TareaCódigo
Crear una cookiesetcookie('tema', 'oscuro', ['expires' => time() + 86400, 'path' => '/', 'httponly' => true, 'samesite' => 'Lax'])
Leer una cookie$_COOKIE['tema'] ?? 'claro'
Borrar una cookiesetcookie('tema', '', ['expires' => time() - 3600, 'path' => '/'])
Empezar o reanudar la sesiónsession_start()
Guardar / leer en sesión$_SESSION['usuario'] = 'marta' / $_SESSION['usuario'] ?? null
Hash de contraseñapassword_hash($clave, PASSWORD_DEFAULT)
Comprobar contraseñapassword_verify($clave, $hash)
Al iniciar sesiónsession_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

[PHP] ¿Dónde se almacenan realmente los datos de $_SESSION?

[PHP] ¿Qué muestra este código?
$h1 = password_hash('x', PASSWORD_DEFAULT);
$h2 = password_hash('x', PASSWORD_DEFAULT);
var_dump($h1 === $h2);

[PHP] ¿Para qué sirve session_regenerate_id(true) justo al iniciar sesión?

[PHP] Haces setcookie('idioma', 'gl', ...) y en la línea siguiente lees $_COOKIE['idioma']. ¿Qué pasa?

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.