Front & Back

Blog sobre el Desarrollo de aplicaciones web

Back end Implementando sesiones en web sockets con Node.js y Socket.io

Sin un inicio de sesión previo, un socket establecido mediante Socket.IO con un servidor Node.js, se encuentra abierto y disponible para cualquiera que escuche. En este documento se implementa una validación previa que "securiza" la conexión con el socket.

Proceso de validación

1. El cliente envía login y password a una url concreta.
2. El servidor valida las credenciales y almacena un token de sesión.
3. El servidor genera una cookie con el token de sesión y la envía al cliente.
4. Durante la navegación el cliente envía la cookie con cada establecimiento de conexión con el socket.
5. El servidor recibe el token de sesión a través de la cookie, lo coteja con las sesiones almacenadas y si está activa abre el socket.

Implementación en el servidor

server.js

/*
Incluímos los paquetes necesarios que hemos instalado previamente:
npm install express
npm install socket.io
npm install cookie*/

var express = require('express');
var io = require('socket.io');
var cookie = require('cookie'); //Para parsear las cookies que vengan en la request
var loginController = require('./controllers/loginController.js');

//Server---------------------------------------------

var PORT = 8888;
var app = express();
var session = new Object(); //En esta variable se almacenarán los tokens de sesión

//Añadimos estas funciones a "app" para recibir los valores enviados por POST
app.use(express.bodyParser());
app.use(express.methodOverride());

//Levantamos el servidor
var server = app.listen(PORT, function() {
loginController.controller(app, session); //Incluimos los controladores para el inicio de sesión
console.log("Listen on port " + PORT);
});

app.use("/js", express.static(__dirname + '/static/js')); //Hacemos público el directorio que contiene el Javascript de cliente

//IO---------------------------------------------------

var io = io.listen(server); //Iniciamos Socket.IO

io.configure(function () {

io.set('authorization', function (handshakeData, callback) {
var cookies = cookie.parse(handshakeData.headers.cookie); //Parseamos las cookies enviadas en la request
var sessionId = cookies['sessionId']; //Obtenemos el token de sesión

if(session[sessionId])
{
callback(null, true); //Si el token de sesión enviado en la cookie está almacenado, permitimos la conexión con el socket
}
else
{
return callback(null, false); //Denegamos la conexión
}
});
});

var socket = io.sockets.on('connection', function(socket) {
console.log('Client connected to socket'); //Se establece la conexión con el socket sólo si el token de sesión es correcto
});

En este ejemplo los tokens de sesión se almacenan en un array, lo que sólo es válido para un ejemplo. En un entorno real, las sesiones deberían ser gestionadas con Redis (por ejemplo), controlando su caducidad, que además debería coincidir con la caducidad de la cookie.

controllers/loginController.js
var crypto = require('crypto'); //Incluímos el módulo encargado de la encriptación del token

function controller(app, session) {

app.get('/login', function (req, res) { //Si accedemos al login mediante el método GET renderizamos el formulario de login
res.render('login.jade');
});

app.post('/login', function (req, res) { //Si se accede por POST se recogen los datos para iniciar la sesión

if(req.body.user == 'usr' && req.body.password == 'pepito') { //Se han "hardcodeado" los datos a modo de ejemplo
var timestamp = new Date().getTime();
var rand = Math.floor(Math.random()*11);
var sessionId = crypto.createHash('md5').update(timestamp.toString()+rand.toString()).digest('hex'); //Generamos el Token

session[sessionId] = timestamp; //Se almacena el token en un array

res.writeHead(200, { //Si la validación es correcta, se adjunta una cookie con el token en la respuesta
'Set-Cookie':'sessionId='+sessionId+'; expires='+new Date(new Date().getTime()+86409000).toUTCString()
});
res.end("Successfully validated!\n");
}
else {
res.writeHead(403);
res.end("Authentication failure\n");
}
});

}

module.exports.controller = controller;

En un entorno real, la validación de usuario y contraseña debería hacerse contra la base de datos MySQL (por ejemplo) en lugar de hacerlo sobre datos "hardcodeados" como se indica, utilizando (por ejemplo) el ORM Sequelize.