Introducción a node.js y express
cookies y sesiones
HTTP es un protocolo sin estado (stateless), esto significa que cuando se carga una página en el navegador, y luego se navega a otra página en el mismo sitio web, ni el servidor ni el navegador tiene alguna forma de saber que es el mismo navegador el que visita el mismo sitio.
Para que pueda reconocer a un cliente que ya pasó por él, necesita que el cliente se identifique de alguna manera. Si el cliente visita por primera vez el servidor y le enviamos algo que le sirva para ser identificado en posteriores visitas habremos conseguido nuestro propósito. Esto es la esencia de las cookies. Una cookie no es más que un pequeño texto que el servidor envía para su almacenamiento en el cliente de manera que este pequeño texto vuelve al servidor cada vez que el mismo cliente vuelve a acceder al mismo servidor. La cookie solo es enviada en peticiones al mismo servidor que la puso. Una cookie nunca es enviada a un servidor del cual no provenga. Las cookies tienen un periodo de validez temporal, pasado el cual la cookie deja de estar activa de forma automática.
Sobre las cookies se debe saber que no son secretas para el cliente. En el navegador del cliente se puede ver el contenido de las cookies de cada sitio, pueden ser modificadas e incluso eliminadas, el cliente puede incluso desactivar la recepción de ellas, por lo tanto, no se debe confiar en el contenido de una cookie cuando vuelve al servidor. Hay un tipo particular de cookie que es la cookie firmada (signed) que ofusca (dificulta su interpretación) el contenido de la misma pero no impide el acceso a ella. Si hacemos uso de ellas y además forzamos la transmisión a través de canales https (secure cookie), dificultaremos la manipulación por terceros.
La gestión de cookies en node.js se hace de forma muy sencilla mediante el middleware cookie-parser. Las cookies recibidas desde el cliente se recuperan desde req.cookies y las cookies de envían al cliente desde res.cookie. En el siguiente ejemplo se ve un uso sencillo de cookies, sirve para contabilizar las visitas que ha realizado un mismo cliente a la página raíz del servidor.
const express = require("express"); const app = express(); const cookieParser = require("cookie-parser"); app.use(cookieParser()); app.get("/", (req, res) => { let intVeces = (req.cookies.veces) ? (parseInt(req.cookies.veces) + 1) : 1; res.cookie("veces", intVeces, { maxAge: (Date.now() + 100000) }); res.send("visitas:" + intVeces.toString()); }) app.use((req, res, next) => { res.send(404, "recurso no encontrado"); // termina }); app.listen(8080, () => { console.log('servidor funcionando en puerto 8080'); })
La fecha de caducidad de la cookie se fija con maxAge, expresada en milisegundos. Para borrar una cookie se invoca el método clearCookie del objeto response. Si queremos ofuscar el contenido de la cookie (signed cookie) debemos incluir
como argumento una cadena, elegida por nosotros que va a servir para encriptar su contenido, en el lanzamiento del middleware cookie-parser. Es una especie de semilla. Recuperaremos las cookies desde req.signedCookies,
e indicaremos que es una cookie signed cuando se envíe al cliente:
const express = require("express"); const app = express(); const cookieParser = require("cookie-parser"); app.use(cookieParser("a45Gtfs#as@43")); app.get("/", (req, res) => { let intVeces = (req.signedCookies.veces) ? (parseInt(req.signedCookies.veces) + 1) : 1; res.cookie("veces", intVeces, { maxAge: (Date.now() + 100000), signed: true }); res.send("visitas:" + intVeces.toString()); }) app.use((req, res, next) => { res.send(404, "recurso no encontrado"); // termina }); app.listen(8080, () => { console.log('servidor funcionando en puerto 8080'); })
Si queremos que la cookie solo se envíe a través de ssl o https añadimos la propiedad secure: true en las opciones de la cookie. Si queremos impedir que la cookie se modifique mediante javascript en el cliente añadimos la propiedad httpOnly:true a opciones de la cookie.
Se llama sesión al intervalo que va desde que el cliente accede por primera vez a una página de un determinado sitio web hasta que cierra el navegador. Durante este intervalo el servidor sabrá que es el mismo cliente el que está accediendo a sus páginas porque al inicio le habrá puesto una cookie llamada de sesión que lo identifica. Esta cookie tiene de vida lo que esté abierto el navegador. Una sesión se puede dar por finalizada también desconectando de forma explícita al cliente desde el servidor y también puede terminarse porque el cliente lleva un determinado tiempo sin acceder al servidor aunque no cierre el navegador.
Cuando el servidor activa una sesión, habilita una zona de almacenamiento específica para la sesión, en la que se almacenará la información que el desarrollador crea conveniente pero que es específica de cada cliente. Este almacenamiento se puede hacer en memoria (si el número de conexiones simultáneas y la información a almacenar no es muy grande), o en bases de datos en caso contrario.
El middleware express-session nos permite gestionar las sesiones de una forma muy simple. El acceso a la información particular de cada cliente se hace a través de la propiedad session (que aparece tras lanzar el middleware) del objeto request.
Cuando se lanza el middleware se le puede pasar un objeto de configuración con las siguientes propiedades, entre otras:
- cookie permite configurar la cookie de sesión. Esta propiedad es a su vez un objeto con las mismas propiedades que se han visto anteriormente cuando se han visto las cookies
- secret Es la semilla que se utiliza para encriptar la cookie de sesión en forma similar a como se hacía en las cookie signed.
- store Indica donde se va a almacenar la información de la sesión de un cliente. Por defecto se almacena en memoria (valor MemoryStore) Si se quiere almacenar la información en una base de datos se deberá utilizar el almacén específico del servidor (consúltese la información detallada en https://www.npmjs.com/package/express-session)
- name Permite indicar el nombre de la cookie de sesión. Por defecto es connect.sid
El siguiente ejemplo muestra el número de veces que el cliente ha accedido a la página durante la sesión:
const express = require('express');
const sesion = require('express-session');
const app = express();
app.use(sesion({ cookie: { signed: true }, secret: "a3A$d23jU@", name: "chj" }));
app.get("/", (req, res) => {
req.session.veces = (req.session.veces) ? (req.session.veces + 1) : 1;
res.send("veces: " + req.session.veces);
});
app.use((req, res, next) => {
res.send(404, "recurso no encontrado"); // termina
});
app.listen(8080, () => {
console.log('servidor funcionando en puerto 8080');
})
El objeto req.session además de alojar los datos de cliente dispone de métodos y propiedades adicionales, por ejemplo, la propiedad id que contienen el id de sesión (el contenido de la cookie de sesión) y el método destroy que borra toda la información de la sesión. (consúltese la información detallada en https://www.npmjs.com/package/express-session)
El siguiente es un desarrollo completo de un sistema de acceso restringido a páginas mediante usuario y contraseña y haciendo uso de sesiones:
index.js
const express = require("express"); const handlebars = require("express-handlebars"); const session = require("express-session"); const app = express(); // activar handlebars app.set("view engine", "handlebars"); app.engine("handlebars", handlebars()); // para poder recoger los datos enviados con post app.use(express.urlencoded({ extended: true })); app.use(session({ secret: "234Tr43s@" })); // recursos estáticos app.use(express.static("public")); // página de login app.get("/", function(req, res, next) { res.render("home", { layout: null }); }); // página de recepción de datos de login y verificación de usuario app.post("/", function(req, res, next) { if ((req.body.user == "pp") && (req.body.password == "1234")) { req.session.user = "pp"; res.redirect("/secreto"); } else res.redirect("/"); }); function controlAcceso(req, res, next) { if (!req.session.user) res.redirect("/"); else next(); } // página con acceso restringido app.get("/secreto", controlAcceso, function(req, res, next) { res.render("secreto", { layout: null }); }); //si llega aquí, es una página no encontrada app.use(function(req, res, next) { res.type("text/plain"); res.status(404); res.send("no encontrado"); }); app.listen(8080, () => { console.log('servidor funcionando en puerto 8080'); })
home.handlebars
<html lang="es"> <head> <link rel="stylesheet" href="css/home.css"> <title>Login</title> </head> <body> <div> <h1>Login</h1> <main> <form action="#" method="POST"> <label for="txtUser">usuario:</label> <input type="text" name="user" id="txtUser"/><br/> <label for="txtPassword">password:</label> <input type="password" name="password" ID="txtPassword"/><br/> <input type="submit" value="entrar"/> </form> </main> </div> </body> </html>
secreto.handlebars
<html lang="es"> <head> <link rel="stylesheet" href="css/home.css"> <title>Secreto</title> </head> <body> <div> <h1>Secreto</h1> </div> </body> </html>
En la petición get de la página raíz se muestra el formulario de login solicitando un usuario y una contraseña. En la petición post se comprueban las credenciales y si son válidas se anota en la información de sesión algo que indique que ha superado la validación, en este ejemplo se almacena el nombre de usuario. En cualquier página que tenga el acceso restringido se comprobará primero que la variable de sesión tiene contenido, lo que indicará que ha superado el control acceso, dando acceso a la página. En caso contrario se remite a la página de login. Esto lo realiza la función de callback de middleware controlAcceso.
function controlAcceso(req, res, next) { if (! req.session.user) res.redirect("/"); else next(); }
La petición de páginas que tengan el acceso restringido será siempre:
app.get(path, controlAcceso, function(req, res, next) { // código de respuesta a la petición res.render(pagina)); });