Sesiones y autenticación — Express Session
Al finalizar este tema
Comprenderá por qué son necesarias las sesiones, podrá implementar el inicio y cierre de sesión con express-session y conocerá el concepto de almacén de sesiones (session store) y su configuración de seguridad.
¿Por qué son necesarias las sesiones?
En el tema de cookies aprendió que "HTTP es sin estado". Aunque las cookies permiten mantener el estado, tienen limitaciones críticas.
// Mal — Almacenar información del usuario directamente en la cookie
res.cookie('user', JSON.stringify({ id: 1, role: 'admin' }));El cliente (navegador) puede modificar las cookies libremente. ¿Qué ocurre si el usuario cambia role: 'admin' por role: 'superadmin' y lo envía? El servidor no puede distinguirlo.
Las sesiones resuelven este problema. Los datos sensibles se almacenan en el servidor y al cliente solo se le entrega una clave de identificación (ID de sesión).
Método de cookies: Almacena "nombre=Hoon, rol=admin" en el cliente (sujeto a falsificación)
Método de sesión: Almacena solo "sessionID=abc123" en el cliente → El servidor consulta la información del usuario con abc123Funcionamiento de la sesión
1. Solicitud de inicio de sesión
Client → Server: POST /login {username: "Hoon", password: "****"}
2. El servidor crea la sesión
Server: Almacena {userId: 1, rol: "admin"} en el almacén de sesiones
Server: Genera ID de sesión = "abc123"
Server → Client: Set-Cookie: connect.sid=abc123
3. Solicitudes posteriores
Client → Server: GET /dashboard (Cookie: connect.sid=abc123)
Server: Consulta la sesión con abc123 → Verifica {userId: 1, rol: "admin"}
Server → Client: Datos del panelEl cliente solo conoce el ID de sesión, mientras que los datos reales (userId, role) se encuentran en la memoria del servidor o en la base de datos. Si el cliente falsifica el ID de sesión, la autenticación fallará si dicho ID no existe en el almacén de sesiones del servidor.
Instalación y configuración de express-session
npm install express-sessionconst express = require('express');
const session = require('express-session');
const app = express();
app.use(express.json());
app.use(session({
secret: 'my-secret-key-change-this',
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: false, // En producción, usar true
maxAge: 3600000 // 1 hour
}
}));| Opción | Descripción |
|---|---|
secret | Clave para cifrar el ID de sesión. Debe ser larga y aleatoria. |
resave | Indica si se debe guardar la sesión en cada solicitud, incluso sin cambios. Se recomienda false. |
saveUninitialized | Indica si se deben guardar también las sesiones vacías. Si se usa false, la cookie no se emitirá antes del inicio de sesión. |
cookie | Opciones de la cookie de sesión (httpOnly, secure, maxAge, etc.) |
secret nunca debe escribirse directamente en el código (hardcoding). Debe gestionarse mediante variables de entorno:
secret: process.env.SESSION_SECRET || 'fallback-dev-only'Implementación de inicio y cierre de sesión
const users = [
{ id: 1, username: 'alice', password: 'pass123', role: 'admin' },
{ id: 2, username: 'bob', password: 'pass456', role: 'user' }
];
// Inicio de sesión
app.post('/login', (req, res) => {
const { username, password } = req.body;
const user = users.find(u => u.username === username && u.password === password);
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Guardar información del usuario en la sesión
req.session.userId = user.id;
req.session.role = user.role;
res.json({ message: `Welcome, ${user.username}` });
});
// Cierre de sesión
app.post('/logout', (req, res) => {
req.session.destroy(err => {
if (err) {
return res.status(500).json({ error: 'Logout failed' });
}
res.clearCookie('connect.sid');
res.json({ message: 'Logged out' });
});
});req.session es un objeto. Puedes almacenar cualquier propiedad libremente. req.session.destroy() elimina los datos de sesión del servidor.
Middleware de autenticación
Cada vez que se accede a una ruta protegida, es necesario verificar si el usuario ha iniciado sesión. Al implementar esto como un middleware, se puede reutilizar en todas las rutas.
function requireAuth(req, res, next) {
if (!req.session.userId) {
return res.status(401).json({ error: 'Login required' });
}
next();
}
function requireAdmin(req, res, next) {
if (req.session.role !== 'admin') {
return res.status(403).json({ error: 'Admin access required' });
}
next();
}
// Uso
app.get('/profile', requireAuth, (req, res) => {
res.json({ userId: req.session.userId, role: req.session.role });
});
app.get('/admin/dashboard', requireAuth, requireAdmin, (req, res) => {
res.json({ message: 'Admin dashboard' });
});Es posible crear condiciones compuestas, como "inicio de sesión + administrador", mediante el encadenamiento de middlewares.
Almacén de sesiones: memoria frente a almacenamiento externo
De forma predeterminada, express-session almacena las sesiones en la memoria del servidor. Esto es suficiente para el desarrollo, pero presenta problemas en producción.
| Problema | Descripción |
|---|---|
| Reinicio del servidor | La memoria se inicializa, por lo que todos los usuarios cierran sesión. |
| Fuga de memoria | Si las sesiones se acumulan, la memoria del servidor puede agotarse. |
| Múltiples servidores | El servidor B no reconoce las sesiones almacenadas en la memoria del servidor A. |
En producción, se utilizan almacenes externos como Redis, MongoDB o PostgreSQL:
npm install connect-redis redisconst RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
const redisClient = createClient();
redisClient.connect();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false
}));Almacenar las sesiones en Redis permite que estas persistan incluso tras reiniciar el servidor, y permite compartir sesiones entre varios servidores si todos apuntan al mismo Redis.
Sesiones vs. JWT: ¿cuándo usar cada una?
| Sesión | JWT | |
|---|---|---|
| Estado | Servidor (stateful) | Cliente (stateless) |
| Escalabilidad | Requiere un almacenamiento compartido como Redis | No requiere compartir entre servidores |
| Cierre de sesión | destroy() Invalidación inmediata | Válido hasta la expiración del token (requiere lista negra) |
| Seguridad | El servidor gestiona los datos | Difícil de mitigar en caso de robo del token |
| Casos de uso adecuados | Aplicaciones web tradicionales, paneles de administración | Servidores API, aplicaciones móviles, microservicios |
Aplicaciones web tradicionales (SSR) → Las sesiones son naturales
SPA + Servidor API → JWT es conveniente
Aplicaciones móviles → JWT (la gestión de cookies es complejaResumen clave
| Concepto | Resumen |
|---|---|
| Sesión | Almacena los datos del usuario en el servidor y solo envía el ID al cliente |
| ID de sesión | Clave de identificación transmitida mediante cookies (connect.sid) |
req.session | Objeto para leer y escribir datos de la sesión |
destroy() | Cierre de sesión: elimina los datos de la sesión |
| Almacén de sesiones | En entornos de producción, se utilizan almacenes externos como Redis |
El principio fundamental de la autenticación basada en sesiones: los datos sensibles se quedan en el servidor y al cliente solo se le entrega la llave (ID de sesión). Al comprender este principio, se pueden entender naturalmente las diferencias con la autenticación basada en tokens (JWT).