Na primeira parte desta série de tutoriais eu lhe mostrei como configurar um projeto Node.js que funcionasse tanto como web server (Express) quanto como um web sockets server (WS). Nesta segunda parte, vamos fazer com que nosso web socket server possa enviar mensagens para os clientes conectados além de aprender como colocar segurança nele.
Veremos:
Vamos lá!
#1 – Enviando mensagens assíncronas
Na lição anterior vimos como facilmente podemos mandar mensagens síncronas de volta para o websocket client. Mas e quando queremos mandar mensagens assíncronas, que é o grande “barato” de ter um websocket server? Neste caso, podemos implementar uma função de envio que pode ser disparada a qualquer momento e que não depende de recebermos qualquer comunicação do cliente.
Abaixo, uma implementação simples de função broadcast, que avisa todos os clientes conectados de alguma novidade.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
function broadcast(jsonObject) { if (!this.clients) return; this.clients.forEach(client => { if (client.readyState === WebSocket.OPEN) { client.send(JSON.stringify(jsonObject)); } }); } module.exports = (server) => { const wss = new WebSocket.Server({ server }); wss.on('connection', onConnection); wss.broadcast = broadcast; console.log(`App Web Socket Server is running!`); return wss; } |
Nesta função, se tiver clientes conectados vamos enviar uma mensagem para cada cliente com conexão aberta (OPEN). Você pode usar este exemplo de broadcast para criar outros tipos de lógicas para enviar a clientes específicos, desde que na conexão de cada cliente você guarde a identidade de cada um deles associado ao objeto ws presente no array this.clients.
Esta função será injetada no objeto wss o qual temos acesso lá na nossa index.js. No exemplo abaixo, vamos enviar uma mensagem aleatória a cada 1 segundo para todos clientes conectados, usando a função broadcast que criamos.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
//index.js const app = require('./app'); const appWs = require('./app-ws'); const server = app.listen(process.env.PORT || 3000, () => { console.log(`App Express is running!`); }) const wss = appWs(server); setInterval(() => { wss.broadcast({ n: Math.random() }); }, 1000) |
Agora, se você se conectar usando o Postman se conectar no servidor, passará a receber um número aleatório a cada segundo, independente se tiver mandando uma mensagem para o servidor ou não (quando mandar mensagens receberá o “recebido” como resposta).
Repare que o broadcast envia para todos os clientes conectados e com conexão OPEN. Assim, um cliente que esteja desconectado, só passará a receber as mensagens depois de se conectar e somente as próximas mensagens, não existe gestão de histórico, algo que você pode implementar caso deseje usando filas com RabbitMQ ou AWS SQS por exemplo (clique nos links para tutoriais de filas em Node.js).
#2 – Segurança em Servidor WS
E se você estiver construindo um servidor de websockets profissional, certamente não irá querer deixar ele aberto pra todo mundo, certo? Por mais que o protocolo websockets seja bem leve, geralmente ficar enviando dados da sua aplicação para anônimas não costuma ser uma boa ideia, a menos que seu server seja exatamente para isso (como as streams de criptomoedas da Binance). Então como podemos implementar segurança em nosso websocket server?
Lembra que mencionei na primeira parte que usaríamos o websocket server integrado ao Express para ter alguns recursos a mais? Pois é, um deles é a rota de login. Hoje ela retorna um token sempre, mas ali você pode implementar a sua lógica de autenticação e só devolver o token (recomendo JWT) em caso de login com sucesso. Uma vez com este token recebido e armazenado no cliente, ele deverá ser usado para realizar uma conexão com seu servidor de websockets.
Vamos ver isso na prática!
Um servidor de websockets que deseja verificar a autenticidade de um cliente deve fazê-lo antes da conexão, no momento que chamamos de handshake em redes de computadores. Na biblioteca WS isso é implementado através de uma função verifyClient que deve ser passada logo na configuração do servidor e que será disparada ANTES de qualquer conexão ser estabelecida, em uma fase de autenticação mesmo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
//app-ws.js function verifyClient(info, callback) { const chave = info.req;//alguma lógica aqui if (token) { //valida a chave return callback(true); } return callback(false); } module.exports = (server) => { const wss = new WebSocket.Server({ server, verifyClient }); wss.on('connection', onConnection); wss.broadcast = broadcast; console.log(`App Web Socket Server is running!`); return wss; } |
Assim, na função verifyClient acima você tem acesso a um objeto info.req com todos os dados da requisição HTTP que acabou de chegar para o handshake e uma função de callback para dar uma resposta se o servidor irá ou não aceitar esta conexão. Uma vez que você aceite a conexão, passando true ao callback, esse verifyClient não será mais executado, mas sim o onConnection e mais tarde as demais funções onMessage e onError.
O céu é o limite no que tange o que você pode implementar nesta função verifyClient, mas aqui vai um exemplo simples que implementa uma lógica de CORS e de token para garantir que somente clientes da nossa origem válida (lembra que definimos ela no .env, na parte 1 do tutorial?) e que possuam um token válido é que podem prosseguir (você poderia usar validação de JWT aqui).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function corsValidation(origin) { return process.env.CORS_ORIGIN === '*' || process.env.CORS_ORIGIN.startsWith(origin); } function verifyClient(info, callback) { if (!corsValidation(info.origin)) return callback(false); const token = info.req.url.split('token=')[1]; if (token) { if (token === '123456') return callback(true); } return callback(false); } |
Repare que agora já temos alguma segurança embora mínima. Se você reiniciar o seu servidor e testar novamente com o Postman verá que o botão de conexão parece não funcionar mais. Isso porque para poder se conectar, agora você tem de informar um token válido na querystring, no formato ws://localhost:3000?token=123456
Aí sim, uma vez autenticado, você passará a receber os dados enviados pelo servidor WS e também poderá enviar mensagens para ele novamente.
E com isso finalizamos mais esta etapa. Neste outro tutorial eu vou lhe mostrar como você pode criar um cliente console para se comunicar com este servidor de websockets e neste aqui, um cliente web. Agora se quiser colocar seu servidor de websockets em produção, recomendo este tutorial de deploy na AWS.
Até lá!
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.
É seguro enviar o JWT pela query String?
Se estiver usando SSL/HTTPS é sim, pois será criptografado durante o transporte do cliente para o servidor e vice-versa. Caso contrário não é. Os mesmos pontos valem para os cabeçalhos HTTP e o body da request, sendo assim, em produção use sempre SSL/HTTPS.