Processamento assíncrono de tarefas com filas no RabbitMQ e Node.js

Um caso de uso de Node.js muito comum, como já expliquei em outros artigos antes, é o de construção de webapis e microsserviços que aguentam grande carga de requisições. Até aqui, isso não deve ser novidade para você.

No entanto, uma fraqueza do Node (e do JavaScript em geral) é a sua performance não tão boa quando o assunto é processamento pesado e/ou tarefas bloqueantes que demoram um tempo razoável para serem concluídas. Isso no Node.js pode ser um grande ofensor uma vez que o event loop trabalha com single thread, certo? Bloqueie esta thread principal e os demais clientes chamando sua API terão uma experiência bem ruim…

Seja nos casos em que o volume de requisições exceda a sua capacidade de resolvê-las rapidamente ou nos casos em que o processamento seja demorado, adotar uma arquitetura que opere de forma assíncrona não apenas garante que você vai conseguir atender todas requisições como vai fazê-lo em um tempo adequado.

No tutorial de hoje vou falar de como construir uma arquitetura simples, porém robusta, usando Node.js e RabbitMQ para processamento assíncrono de requisições recebidas em uma web API RESTful.

Processamento Assíncrono

O primeiro conceito que você tem de entender é que apesar do HTTP ser síncrono, já faz quase 20 anos que a Internet (e os sistemas que rodam nela) já entendeu que trabalhar de maneira assíncrona é uma forma de proporcionar experiências cada vez melhores aos usuários.

Em uma requisição síncrona, a request é enviada ao servidor, que a processa e devolve uma response. Tudo de uma vez só. Enquanto a response não é retornada, a conexão fica presa e o usuário fica esperando. Se demorar demais, a conexão pode ser encerrada abruptamente e o cliente terá de fazê-la de novo, sem saber exatamente o que acontecer nesta segunda chamada.

Em uma requisição assíncrona, a request é enviada ao servidor, que registra a mesma e automaticamente responde ao cliente que vai realizar a tarefa em breve, avisando-o de alguma maneira quando ela for concluída. Um outro processo cuida de processar essa requisição armazenada e notificar o cliente de alguma forma, se necessário.

A imagem abaixo ilustra um pouco dessa diferença.

Sync vs Async
Sync vs Async

Obviamente o segundo modelo é um pouco mais complexo de lidar, mas possui algumas vantagens muito interessantes.

Enquanto que processamento síncrono dá uma resposta mais direta e rápida para o cliente quando o servidor não está sobrecarregado de requisições, é quando a carga de chamadas às suas APIs é muito alta que ele se mostra inviável. Neste caso, apenas registrar as requisições, para processá-las em uma fila, por exemplo, garante que todos vão ser atendidos, cada um no seu tempo e nenhuma request vai ser dropada.

Assim, pensando nessa arquitetura, eu proponho neste tutorial o uso de RabbitMQ, uma tecnologia gratuita e open-source escrita em Erlang para ser usada como fila para as suas requisições, visando processamento assíncrono pelo Node.js.

RabbitMQ

O MQ no nome do Rabbit vem de Message Queue ou Fila de Mensagens, o que é exatamente o que ele é. O RabbitMQ é hoje a tecnologia de fila mais popular do mercado e fornece integração com todas as tecnologias comerciais mais utilizadas, incluindo aí Node.js. Ele é construído com a linguagem funcional Erlang e implementando o protocolo AMQP (Advanced Message Queue Protocol ou Protocolo Avançado de Filas de Mensagens).

Um dos possíveis casos de uso do RabbitMQ é a construção de arquiteturas para processamento assíncrono usando o padrão Producer/Consumer (Produtor/Consumidor), onde de um lado eu vou ter um produtor que envia mensagens pra uma fila (queue) e de outro lado um consumidor que é notificado que uma nova mensagem chegou, para processá-la. Existem arquiteturas mais complexas possíveis de serem feitas, com N para N, mas meu intuito aqui é manter simples neste primeiro momento.

Para efeitos mais práticos e simples, imagine que de um lado eu tenho uma API (producer) que recebe uma grande carga de requisições, ela enfileira essas requisições no Rabbit e um worker (consumer) vai processando essas mensagens uma a uma, conforme ele vai conseguindo dar conta.

A imagem abaixo ilustra uma possibilidade como essa, onde aquele canal no meio é onde fica o RabbitMQ.

Producer/Consumer
Producer/Consumer

Para rodar o RabbitMQ na sua máquina você vai precisar ter o Erlang instalado e depois pode baixar o Rabbit e executá-lo via linha de comando mesmo. A área de downloads dá as melhores instruções para instalação conforme o seu sistema operacional.

Com tudo instalado, eu costumo subir um servidor de filas do Rabbit usando o utilitário rabbitmq-server dentro da pasta sbin, como no comando abaixo (em Windows ajuste o path do cd e você não precisa do ./ no início do comando também)

Quando o servidor sobe corretamente você deve ver algo parecido com a imagem abaixo no seu terminal.

RabbitMQ funcionando
RabbitMQ funcionando

Cliente RabbitMQ em Node.js

O RabbitMQ possui clientes nas mais variadas tecnologias, incluindo Node.js como já mencionei antes. Uma vez com o servidor do RabbitMQ rodando, crie uma nova pasta e rode um npm init para iniciar um novo projeto Node.js nela.

Você tem de instalar as seguintes dependências no seu projeto:

  • express
  • body-parser
  • amqplib

Vamos criar um módulo JavaScript que vai encapsular a nossa lógica de produzir e consumir mensagens, bem como de criar a fila no Rabbit. Esse módulo depois será usado pela webapi e pelo worker (note que uso promises aqui).

Esse módulo é bem simples, possui 4 funções e expõe apenas duas, uma que vai ser usada pelo producer (sendToQueue) e outra pelo consumer (consume).

A primeira função, connect, é autoexplicativa, ela carrega a dependência do pacote amqplib e conecta-se na URL do servidor RabbitMQ, retornando o canal de comunicação. Essa função é usada pelas demais e é a recomendação, abrir sempre o canal de comunicação para garantir que não terá problemas com conexão.

A segunda função createQueue, é chamada pelas outras duas para garantir a existência, mas não se preocupe pois ela é idempotente, ou seja, depois de criada, ela não criará repetida e nem dará erro de que já existe. É mais uma recomendação de boa prática.

A terceira função, sendToQueue, será usada pelo producer (webapi) e na minha implementação ele está esperando um objeto JSON como argumento da função, sendo que ele serializa o mesmo para ser enviado ao Rabbit. Você pode ter várias filas diferentes, então esta função espera como argumento o nome da fila onde você vai adicionar essa mensagem.

E por fim, a função consume, que será usada pelo consumer (worker). Essa função espera um callback que é a função do cliente dessa lib que vai ser disparada toda vez que entrar uma mensagem nova na fila, é a função que vai processar a requisição agendada.

Agora vamos criar o produtor e o consumidor.

Produtor e Consumidor

Nossa API de exemplo será muito simples, uma vez que o objetivo aqui não é ensinar a criar webapis. Apenas copie e cole o código abaixo em um arquivo webapi.js na raiz do projeto:

Essa API espera um POST em um endpoint /task com um JSON no body da requisição. Ela apenas pega esse body e joga pra fila.

Da mesma forma, nosso worker de exemplo será muito simples, uma vez que não é o foco do artigo. Apenas copie e cole o código abaixo em um arquivo worker.js na raiz do projeto:

Esse worker é muito simples, ele apenas fica escutando a fila1 (a mesma em que o producer vai jogar as mensagens) e quando chega alguma coisa lá, o callback pega a mensagem e apenas imprime o conteúdo no console.

Testando e Além

Uma vez que você já tenha o servidor de RabbitMQ rodando, testar é muito simples, basta subir a webapi via terminal e depois subir o worker em outro terminal, em qualquer ordem.

Você deve começar o teste pelo producer, ou seja, abra o POSTMAN e envie um objeto JSON qualquer via POST para localhost:3000/task que isso deve disparar a mensagem pra fila que deve ser consumida pelo worker quase imediatamente.

Claro, este é um consumer meeega simples. Consumers reais vão processar dados da mensagem, fazer operações em banco e até mesmo chamar outras APIs se necessário, principalmente para avisar que essa requisição já foi processada. Você pode querer também alterar a interface para dar uma resposta ao usuário e por aí vai.

Você pode ainda explorar mais possibilidades desta arquitetura como o padrão com múltiplos consumers concorrentes (1xN) ou ainda o padrão Pub-Sub (Publish/Subscribe) onde podemos ter uma relação de NxN.

Mas enfim, a ideia deste artigo era te ajudar a fazer o Rabbit funcionar com Node.js e espero que você tenha conseguido. Caso contrário, apenas baixe os meus fontes usando o formulário ao final do tutorial.

Até a próxima!

Curtiu o post? Então clica no banner abaixo e dá uma conferida no meu curso sobre programação web com Node.js!

Curso Node.js e MongoDB

9 anos de LuizTools

TDC 2019
TDC 2019

Neste dia 19/12/19 o meu blog comemora 9 anos de vida. Ele coincide com o tempo que tenho de graduado em Ciência da Computação, pois foi justamente quando me formei na faculdade que decidi que queria compartilhar o que sabia e o que estava recém aprendendo com outras pessoas.

Quando eu comecei, mexia apenas com C#, basicamente, e minha ideia era apenas compartilhar (e até mesmo arquivar) alguns códigos e projetos que poderiam ser úteis para outros desenvolvedores. As ferramentas do Luiz. No entanto, logo o blog se tornou mais do que isso e passei a estudar outras plataformas e a ensinar o pouco que sabia sobre elas por aqui também.

Hoje, 323 posts depois, o blog que já passou por várias reformulações, tem duas linhas claras de conteúdo: desenvolvimento de software (atualmente em Node.js) e métodos ágeis. Quer você goste de um assunto, ou de outro, semanalmente tem um post novo aqui no blog e minha newsletter chega à caixa de email de mais de 14 mil assinantes (no final desse post tem um formulário para se inscrever).

Vou aproveitar esta oportunidade para compartilhar alguns números com os leitores, bem como os melhores posts aqui do blog, baseados no número de leituras que cada um teve.

Você sabia que…

  • todos os meses, 37 mil leitores visitam o meu blog?
  • que 75% dos meus leitores são homens e que quase metade deles tem entre 25 e 34 anos?
  • que 5% dos meus leitores são de fora do país?
  • que SP, RJ e MG, nessa ordem, são os estados com mais visitantes no meu blog?
  • que 81% dos visitantes usam o Google Chrome para ler meus posts?
  • que 34% dos meus leitores usam um dispositivo móvel para ler meus posts?
  • que menos de 2% do meu tráfego vem das minhas redes sociais, como Facebook, Youtube, LinkedIn e Twitter (me segue aí!)?

Você já leu estes posts aqui? São os que o pessoal mais gosta dentre todos os 323 escritos até o momento:

E você sabia que já lancei diversos livros que surgiram a partir de conteúdos aqui do blog? Vários deles são best-sellers da Amazon e já venderam mais de 6.793 cópias juntos!

Além disso possuo dois cursos online à venda aqui no blog, sobre minhas duas áreas de expertise atualmente, sendo que já possuo mais de 466 alunos somados!

E que venha 2020 e o décimo ano de vida do blog!

Autenticação JSON Web Token (JWT) em Node.js – Parte 2

JSON Web Token, o famoso JWT, é de longe a forma mais comum de segurança utilizada em web APIs RESTful depois do uso de SSL/TLS. Quando temos variados clientes consumindo nossas APIs, saber quem está autenticado ou não, ou ainda, que tem autorização ou não para fazer as chamadas, é importantíssimo.

Na primeira parte deste tutorial eu ensinei como implantar um mecanismo de JWT em uma API Node.js, adicionando esta importante camada de segurança. Dentre as dúvidas mais comuns acerca desta técnica está a segurança do token, afinal ele é o ponto mais exposto na técnica e sequestro de tokens é a forma mais comum de tentar burlar este mecanismo.

Claro que usando SSL (sugiro Lets Encrypt que é gratuito) esse risco de captura diminui consideravelmente uma vez que a conexão está criptografada e encorajo fortemente você a não aceitar requisições usando HTTP. No entanto, sabemos que para tudo existem brechas a serem exploradas principalmente por parte de pessoas maliciosas dentro da sua própria empresa muitas vezes…

O risco do JWT

Mas o token não é criptografado? Eu olhei ele e vi um monte de letras e números aleatórios…

Não, o token apenas é codificado em base64, uma representação textual de um conjunto de bytes, se você jogá-lo em qualquer decodificador online, verá as três partes que o compõem sendo que a única ilegível é a terceira onde temos a assinatura digital do servidor, atestando que aquele token foi gerado corretamente pelo seu servidor, o que impede que tokens fake se passem por tokens reais.

Desta forma, se o seu token for capturado (de alguma maneira mirabolante), durante o seu prazo de validade, ele poderá ser usado para fazer chamadas em seu nome e isso certamente não é um risco que você queira correr principalmente em aplicações mais visadas, como soluções bancárias (com as quais trabalho desde 2017).

Antes de eu entrar na solução para mitigar esse risco, vale lembrar que é importante você ter uma maneira fácil e rápida de invalidar tokens em caso de fraude de chamadas, para que a área de segurança possa agir rapidamente nestas situações. Falarei mais sobre isso no futuro.

Mas voltando ao assunto central: como podemos adicionar mais segurança em nosso JWT?

Comprovando a vulnerabilidade do JWT

Para comprovarmos isso na prática, primeiro compartilho abaixo uma pequena API que implementa este mecanismo, para que você possa acompanhar meu raciocínio sem precisar refazer todo tutorial anterior.

Para fazer funcionar este código, você terá de instalar os seguintes pacotes com o comando abaixo:

Se você rodar este projeto simples ele deve funcionar como esperado: a rota clientes só pode ser acessada com um JWT válido. Para obter um, POST na rota de login com usuário e senha corretos no corpo.

Se pegarmos o token abaixo…

E jogarmos em um decodificador de base64 online teremos…

A primeira parte é o header, a segunda é o token e a terceira é a assinatura do servidor (criado usando o secret/segredo configurado no servidor). Note a informação sensível do id no token, mas que poderia ter outras informações também, relacionadas ao cliente que está consumindo a API como perfil de acesso e outros.

Mas como podemos garantir que este token não possa mais ser lido de maneira aberta como essa?

Criptografando o JWT

A solução mais prática para adicionar mais proteção aos eu token é criptografá-lo. Como estou falando de um token que navega entre cliente e servidor por meio inseguro (internet) o ideal é uma criptografia assimétrica, onde o cliente usará a chave pública do servidor para lhe mandar o token, que só poderá ser decifrado com a chave privada, de posse SOMENTE do servidor.

Assim, mesmo que um token seja capturado, o seu conteúdo não poderá ser decifrado sem conhecer esta chave privada.

Primeiro, vamos criar um par de chaves (uma pública e outra privada) usando o algoritmo RSA, um dos mais famosos e seguros do mundo de tipo assimétrico. O jeito mais fácil de gerar um par para fins de estudo é usando um gerador online, como esse aqui. O Format Scheme é PKCS #1 e o tamanho da chave varia de 256 bits a 2048 e embora chaves maiores sejam mais seguras (cada vez que você dobra o tamanho multiplica por 6x a dificuldade de quebra da chave) atente ao fato de que seu JWT deverá ser decifrado pelo servidor a cada requisição e que chaves maiores são mais demoradas para decifrar, mesmo com a chave certa.

Como é apenas para estudo, fiz com o menor tamanho comercialmente aceito (1024, enquanto que 2048 é o mais recomendado até 2030) e minha chave pública ficou assim (salve em um arquivo public.key):

Enquanto que minha privada ficou assim (salve em um arquivo private.key):

Agora, em nossa API, vamos mudar levemente o nosso código que gera os tokens para que os mesmos sejam criptografados.

As alterações estão apenas a partir da linha que declaro a privateKey, que substitui o nosso secret padrão que estávamos usando. A leitura do arquivo da chave é feita usando módulo fs (adicione um require no topo do arquivo) e nas opções de assinatura (terceiro argumento da função) dizemos o algoritmo de hashing que o RSA vai usar no seu algoritmo interno (RS256 no meu caso represa SHA-256).

Atenção: caso o seu arquivo de chave secreta tenha senha (como são geralmente os .PEM gerados pelo OpenSSL, que é a ferramenta que recomendo que você use) você deve passar um objeto {key, passphrase} no segundo argumento do sign ao invés de apenas a privatekey.

Agora, ao se autenticar, o servidor lhe retornará um token criptografado, muuuito mais difícil de ter suas informações decifradas pois para isso seria necessário descobrir a outra chave. Neste caso não estamos mais usando base64 e se tentar usar um decodificador online vai ver que ele não consegue ler nada.

Atenção: se você tiver o erro “digest too big for RSA key” quer dizer que sua chave é pequena demais para o texto a ser cifrado. Neste caso, use chaves maiores (eu não tive problema a partir de 1024-bit).

Verificando o JWT

Agora é hora de ajustar o código que decifra o JWT, feito na mesma função que verifica o mesmo. Enquanto que para crifrarmos o token usamos a private key, para verificar o mesmo usamos a public key.

Note que a alteração foi bem sutil mesmo: carregamos a public key a partir do respectivo arquivo, passamos ela pra função verify, bem como um objeto informando o algoritmo de hashing que usamos junto do RSA (RS256 refere-se a SHA-256). Sim, é um array de algoritmos e passei apenas um.

Agora, se você obter um token pela rota de login e usá-lo para acessar a rota de clientes, verá que está funcionando como deveria, mas que o seu JWT está finalmente seguro caso seja capturado por algum atacante.

Note que esta abordagem consome mais recursos computacionais que os tokens abertos. Esteja preparado para um aumento custo de hardware e/ou do tempo entre cada requisição que necessita deste token.

Espero que tenha gostado do artigo!

Curtiu o post? Então clica no banner abaixo e dá uma conferida no meu curso sobre programação web com Node.js!

Curso Node.js e MongoDB