Sesiones y cookies
El modelo stateful clásico: el servidor crea y mantiene el estado de la sesión. La cookie solo transporta un identificador. Los datos reales se guardan en el servidor.
El flujo básico con este modelo es el siguiente:
- Usuario envía credenciales (a través de un formulario de inicio de sesión).
- Servidor verifica y crea una sesión en su almacén (memoria, Redis, base de datos, etc.).
- Servidor envía al cliente una cookie con el Session ID (opaco, aleatorio).
- En cada petición, el navegador envía la cookie automáticamente.
- El servidor busca la sesión por ID, recupera los datos del usuario y procesa la petición.
- Al cerrar sesións, el servidor destruye la sesión. La cookie, aunque se siga enviando, ya no sirve.
Ventajas de las sesiones:
- Logout real e inmediato (destruir la sesión en el servidor).
- El servidor tiene control total sobre la sesión activa.
- Sencillo de implementar para apps web tradicionales.
- Revocación granular (invalidar sesiones concretas).
Desventajas de las sesiones:
- Requiere almacenamiento persistente en el servidor.
- Escala mal sin un store externo compartido (Redis).
- No es la mejor opción para APIs consumidas por móviles o terceros.
- Latencia añadida en cada petición para consultar el store.
Algunas librerías de Node.js para trabajar con sesiones:
express-session: Middleware de sesiones para Express.cookie-session: Sesión almacenada íntegramente en la cookie (stateless, sin store). Útil para payloads pequeños.connect-redis: Store de sesiones en Redis.connect-pg-simple: Store de sesiones en PostgreSQL.connect-mongo: Store de sesiones en MongoDB.
Implementación con express-session
import express from 'express';
import session from 'express-session';
const app = express();
app.use(session({
secret: process.env.SESSION_SECRET, // Clave para firmar la cookie (HMAC)
resave: false, // No re-guardar sesiones sin cambios
saveUninitialized: false, // No crear sesión hasta que haya datos
cookie: {
httpOnly: true, // Inaccesible desde JS del navegador (mitiga XSS)
secure: true, // Solo se envía por HTTPS
sameSite: 'Strict', // Protección contra CSRF
maxAge: 1000 * 60 * 60 * 24 // 24 horas en ms
}
}));
// Login
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = await findUser(username);
if (!user) return res.status(401).json({ error: 'Credenciales incorrectas' });
const valid = await bcrypt.compare(password, user.passwordHash);
if (!valid) return res.status(401).json({ error: 'Credenciales incorrectas' });
req.session.userId = user.id;
req.session.role = user.role;
res.json({ message: 'Login exitoso' });
});
// Middleware de protección de rutas
export const requireAuth = (req, res, next) => {
if (!req.session.userId) {
return res.status(401).json({ error: 'No autenticado' });
}
next();
};
// Ruta protegida
app.get('/profile', requireAuth, async (req, res) => {
const user = await findUserById(req.session.userId);
res.json(user);
});
// Logout
app.post('/logout', (req, res) => {
req.session.destroy(err => {
if (err) return res.status(500).json({ error: 'Error al cerrar sesión' });
res.clearCookie('connect.sid');
res.json({ message: 'Sesión cerrada' });
});
});
Almacenamiento de sesiones
Por defecto, express-session guarda las sesiones en memoria del proceso. Esto es inviable en producción: los datos se pierden al reiniciar el servidor y no funciona con múltiples instancias. En producción se utiliza un store externo:
| Store | Librería | Cuándo usarlo |
|---|---|---|
| Redis | connect-redis | Producción. Alta velocidad, TTL nativo, ideal para escalar horizontalmente. |
| PostgreSQL | connect-pg-simple | Si ya usas PostgreSQL y no quieres añadir Redis. |
| MongoDB | connect-mongo | Si ya usas MongoDB. |
| Memoria | (por defecto) | Sólo desarrollo local. |
import RedisStore from 'connect-redis';
import { createClient } from 'redis';
const redisClient = createClient({ url: process.env.REDIS_URL });
redisClient.connect();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { httpOnly: true, secure: true, sameSite: 'Strict', maxAge: 86400000 }
}));