Cookies, sessões e persistência

Introdução

O HTTP (Protocolo de Transferência de Hipertexto) foi projetado para oferecer suporte a um modelo sem estado de solicitação e resposta para a transferência de dados de um servidor para um cliente. Sua primeira versão, a 1.0, permitia uma proporção estrita de 1:1 entre solicitações e conexões (ou seja, cada conexão permitia apenas um par de solicitação e resposta).

A versão 1.1 ampliou essa proporção para N:1 — ou seja, várias solicitações por conexão. Isso foi feito para lidar com a crescente complexidade das páginas da web, incluindo os diversos objetos e elementos que precisam ser transferidos do servidor para o cliente.

Com a adoção da versão 2.0, o HTTP continuou oferecendo suporte a um modelo com várias solicitações por conexão. Suas mudanças mais radicais envolvem a troca de cabeçalhos e a transição da transferência baseada em texto para a binária.

Em algum momento, o HTTP deixou de ser apenas um mecanismo simples para transferir textos e imagens de um servidor para um cliente e se tornou uma plataforma para aplicações. A onipresença do navegador, sua natureza multiplataforma e a facilidade com que as aplicações podiam ser implantadas sem o alto custo de oferecer suporte a vários sistemas operacionais e ambientes eram, sem dúvida, atrativas. Infelizmente, o HTTP não foi projetado para ser um protocolo de transporte de aplicações. Ele foi projetado para transferir documentos. Até mesmo usos modernos do HTTP, como em APIs, pressupõem uma carga útil semelhante a um documento. Um bom exemplo disso é o JSON, um formato de dados composto por pares de chave e valor, transferido como texto. Embora documentos e protocolos de aplicação geralmente sejam baseados em texto, a semelhança termina aí.

Aplicações tradicionais precisam de alguma forma de manter seu estado, ao contrário dos documentos. As aplicações são construídas com base em fluxos e processos lógicos, e ambos exigem que a aplicação saiba em que ponto o usuário se encontra naquele momento, o que requer estado. Apesar da natureza inerentemente sem estado do HTTP, ele se tornou, na prática, o protocolo de transporte de aplicações da web. No que certamente é uma das soluções improvisadas mais úteis e amplamente aceitas da história da tecnologia, o HTTP recebeu os meios necessários para que o estado pudesse ser acompanhado durante todo o uso de uma aplicação. É aí que entram as sessões e os cookies.

Sessões

Transformando o sem estado em com estado

As sessões são o mecanismo pelo qual os servidores web e de aplicações mantêm o estado. Esses pequenos blocos de memória são associados a cada conexão TCP estabelecida com um servidor web ou de aplicações e funcionam como armazenamento em memória para informações em aplicações baseadas em HTTP.

Quando um usuário se conecta a um servidor pela primeira vez, uma sessão é criada e associada a essa conexão. Os desenvolvedores usam essa sessão para armazenar dados relevantes para a aplicação. Esses dados podem variar de informações importantes, como o ID de um cliente, a dados menos relevantes, como suas preferências de exibição da página inicial do site.

O melhor exemplo da utilidade das sessões são os carrinhos de compras, pois quase todos nós já fizemos compras on-line alguma vez. Os itens de um carrinho de compras permanecem durante uma "sessão" porque cada item do carrinho é representado de alguma forma na sessão no servidor. Outro bom exemplo são os aplicativos de configuração ou personalização de produtos no estilo assistente. Esses "miniaplicativos" permitem navegar por um conjunto de opções e selecioná-las; no final, você geralmente se surpreende com o custo estimado de todos os recursos e extras adicionados. À medida que você avança por cada "tela de opções", as demais opções escolhidas são armazenadas na sessão para que possam ser facilmente recuperadas, adicionadas ou excluídas.

As aplicações modernas são projetadas para não manter estado, mas suas arquiteturas podem não seguir esse princípio. Os métodos modernos de escalabilidade geralmente dependem de padrões arquiteturais como o particionamento (sharding), que exige o roteamento de solicitações com base em algum dado indexável, como o nome de usuário ou o número da conta. Isso requer uma abordagem que, de certa forma, mantém estado, pois os dados indexáveis são enviados com cada solicitação para garantir o roteamento correto e o comportamento adequado da aplicação. Nesse sentido, aplicações e APIs modernas “sem estado” muitas vezes exigem cuidados e manutenção semelhantes aos de suas antecessoras que mantinham estado.

O problema é que as sessões estão vinculadas às conexões, e as conexões que permanecem ociosas por muito tempo expiram. Além disso, a definição de "muito tempo" para conexões é bem diferente de quando ela é aplicada a sessões. A configuração padrão de alguns servidores web, por exemplo, é encerrar uma conexão após 15 segundos de inatividade — ou seja, sem que nenhuma outra solicitação tenha sido feita. Por outro lado, nesses servidores web, uma sessão permanece na memória por padrão durante 300 segundos, ou 5 minutos. Obviamente, essas duas configurações são incompatíveis, pois, quando a conexão expira, de que serve a sessão se ela está associada à conexão?

Você pode pensar que bastaria aumentar o tempo limite da conexão para igualá-lo ao da sessão e resolver essa disparidade. Aumentar o tempo limite significa que você poderá acabar consumindo memória para manter uma conexão que talvez seja usada ou não. Isso pode reduzir a capacidade total de usuários simultâneos do servidor e, em última análise, prejudicar seu desempenho. E certamente não é uma boa ideia reduzir o tempo limite da sessão para igualá-lo ao da conexão, pois a maioria das pessoas leva mais de cinco minutos para pesquisar opções ou personalizar seu novo brinquedo.

Observação importante: Embora o HTTP/2 resolva alguns desses problemas, ele introduz outros relacionados à manutenção de estado. Embora não seja exigido pela especificação, os principais navegadores só permitem HTTP/2 sobre TLS/SSL. Ambos os protocolos exigem persistência para evitar o custo de desempenho da renegociação, o que, por sua vez, requer reconhecimento de sessão, também conhecido como comportamento com estado.

Assim, o resultado são sessões que permanecem na memória do servidor mesmo depois que as conexões associadas a elas são encerradas por inatividade, consumindo recursos valiosos e possivelmente irritando os usuários para os quais seu aplicativo simplesmente não funciona.

Felizmente, esse problema é resolvido com o uso de cookies.

Cookies

O rastro de migalhas leva para casa

Cookies são pequenos conjuntos de dados armazenados pelo navegador no dispositivo do usuário. Eles podem — e de fato armazenam — todo tipo de informação interessante sobre você, seus aplicativos e os sites que visita. O termo "cookie" deriva de "magic cookie", um conceito bastante conhecido na computação UNIX que inspirou tanto a ideia quanto o nome. Os cookies são criados e compartilhados entre o navegador e o servidor por meio do cabeçalho HTTP Cookie.

Cookie: JSESSIONID=9597856473431 Cache-Control: no-cache Host: 127.0.0.2:8080 Connection: Keep-Alive

O navegador sabe automaticamente que deve armazenar o cookie do cabeçalho HTTP em um arquivo no seu computador e gerencia os cookies separadamente para cada domínio. Os cookies de um determinado domínio são sempre enviados pelo navegador ao servidor nos cabeçalhos HTTP, permitindo que os desenvolvedores de aplicações web recuperem esses valores simplesmente solicitando-os no lado do servidor da aplicação.

O problema da duração da sessão/conexão é resolvido por meio de um cookie. Quase todas as aplicações web modernas geram um "ID de sessão" e o enviam como um cookie. Isso permite que a aplicação localize a sessão no servidor mesmo depois que a conexão na qual ela foi criada é encerrada. Por meio dessa troca de IDs de sessão, o estado é mantido até mesmo em um protocolo sem estado como o HTTP. Mas o que acontece quando o uso de uma aplicação web ultrapassa a capacidade de um único servidor web ou de aplicações? Normalmente, é introduzido um balanceador de carga ou, nas arquiteturas atuais, um Controlador de Entrega de Aplicações (ADC), para dimensionar a aplicação de modo que todos os usuários tenham níveis satisfatórios de disponibilidade e desempenho.

Em aplicações modernas, os cookies ainda podem ser usados, mas outros cabeçalhos HTTP tornam-se essenciais. As chaves de API para autenticação e autorização geralmente são transmitidas por meio de um cabeçalho HTTP, assim como outros cabeçalhos personalizados que transportam dados necessários para o roteamento e o dimensionamento adequado dos serviços de backend. Usar o “cookie” tradicional ou algum outro cabeçalho HTTP para transportar esses dados é menos importante do que reconhecer a importância deles para a arquitetura como um todo.

O problema é que os algoritmos de balanceamento de carga geralmente se preocupam apenas em distribuir as solicitações entre os servidores. As técnicas de balanceamento de carga são baseadas em algoritmos padrão do setor, como round robin, menor número de conexões ou menor tempo de resposta. Nenhum deles mantém estado, e é possível que cada solicitação feita pelo mesmo usuário a uma aplicação seja direcionada a um servidor diferente. Isso torna inútil todo o trabalho realizado para implementar estado no HTTP, pois os dados armazenados na sessão de um servidor raramente são compartilhados com os demais servidores do "pool".

É aqui que o conceito de persistência se torna útil.

Persistência

O Laço que Une

A persistência — também conhecida como afinidade — é uma técnica implementada por ADCs para garantir que as solicitações de um mesmo usuário sejam sempre encaminhadas ao servidor em que a sessão foi iniciada. Alguns produtos e serviços de balanceamento de carga descrevem essa técnica como “sessões persistentes”, um nome perfeitamente apropriado.

Há muito tempo, a persistência é usada no balanceamento de carga de sites habilitados para SSL/TLS, pois, depois que o processo de negociação — que exige muitos recursos computacionais — é concluído e as chaves são trocadas, reiniciar o processo prejudicaria significativamente o desempenho. Por isso, os ADCs implementaram a persistência de sessão SSL para garantir que os usuários fossem sempre direcionados ao mesmo servidor ao qual se conectaram inicialmente.

Ao longo dos anos, as implementações dos navegadores tornaram necessário o desenvolvimento de uma técnica para evitar a renegociação dispendiosa dessas sessões. Essa técnica é chamada de persistência baseada em cookies.

Em vez de depender do ID da sessão SSL/TLS, o balanceador de carga inseria um cookie para identificar exclusivamente a sessão na primeira vez que um cliente acessava o site e, nas solicitações subsequentes, consultava esse cookie para manter a conexão com o servidor apropriado.

Desde então, o conceito de persistência baseada em cookies passou a ser aplicado às sessões de aplicativos, usando informações de ID de sessão geradas por servidores web e de aplicativos para garantir que as solicitações dos usuários sejam sempre direcionadas ao mesmo servidor durante uma mesma sessão. Sem esse recurso, os aplicativos que exigem balanceamento de carga precisariam encontrar outra forma de compartilhar informações de sessão ou recorrer ao aumento dos tempos limite de sessão e conexão, a ponto de o número de servidores necessários para atender à sua base de usuários rapidamente se tornar inviável.

Embora a forma mais comum de persistência seja implementada por meio de IDs de sessão transmitidos no cabeçalho HTTP, atualmente os ADCs também podem manter a persistência com base em outros tipos de dados. Qualquer dado que possa ser armazenado em um cookie ou obtido dos cabeçalhos IP, TCP ou HTTP pode ser usado para manter a persistência de uma sessão. Na verdade, qualquer dado contido em uma mensagem de aplicação que identifique o usuário de forma exclusiva pode ser usado por um ADC inteligente para manter uma conexão persistente entre o navegador e um servidor.

Conclusão

O HTTP pode ser um protocolo sem estado, mas conseguimos incorporar o estado à força nesse protocolo onipresente. Com o uso de persistência e de Controladores de Entrega de Aplicações, é possível projetar aplicações web de alto desempenho e alta disponibilidade sem comprometer a integração um tanto frágil entre cookies e sessões, necessária para manter o estado.

São esses recursos que conferem estado ao HTTP, embora sua implementação e execução permaneçam sem estado. Sem cookies, sessões e persistência, certamente teríamos encontrado um protocolo com estado sobre o qual construir nossas aplicações. Em vez disso, os recursos e as funcionalidades presentes nos Controladores de Entrega de Aplicações fazem a mediação entre navegadores (clientes) e servidores para oferecer essa funcionalidade, ampliando a utilidade do HTTP para além das páginas web estáticas e das aplicações tradicionais, abrangendo arquiteturas modernas baseadas em microsserviços e a queridinha da economia digital: a API.

PUBLICADO EM 19 DE JANEIRO DE 2018