Backend Roadmap

Anatomia do Protocolo HTTP

Anatomia do Protocolo HTTP

HTTP REST JAX-RS Java Jakarta-EE

Anatomia do Protocolo HTTP

Introdução

HyperText Transfer Protocol (HTTP) é um protocolo no nível da aplicação para sistemas de informação distribuídos e colaborativos. É um protocolo genérico e stateless que pode ser estendido pela aplicação. O protocolo permite a negociação do formato do dado entre cliente e servidor, permitindo que sistemas sejam implementados independentemente.

RFCs Título Data
2616 Hypertext Transfer Protocol – HTTP/1.1 Junho 1999
2617 HTTP Authentication: Basic and Digest Access Authentication Junho 1999
2817 Upgrading to TLS Within HTTP/1.1 Maio 2000
5785 Defining Well-Known Uniform Resource Identifiers (URIs) Abril 2010
6265 HTTP State Management Mechanism Abril 2011
6266 Use of the Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP) Junho 2011
6585 Additional HTTP Status Codes Abril 2012
6749 The OAuth 2.0 Authorization Framework Outubro 2012
6750 The OAuth 2.0 Authorization Framework: Bearer Token Usage Outubro 2012
7168 The Hyper Text Coffee Pot Control Protocol for Tea Efflux Appliances (HTCPCP-TEA) 1º Abril de 2014
7230 Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routing Junho 2014
7231 Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content Junho 2014
7232 Hypertext Transfer Protocol (HTTP/1.1): Conditional Requests Junho 2014
7233 Hypertext Transfer Protocol (HTTP/1.1): Range Requests Junho 2014
7234 Hypertext Transfer Protocol (HTTP/1.1): Caching Junho 2014
7235 Hypertext Transfer Protocol (HTTP/1.1): Authentication Junho 2014
7519 JSON Web Token (JWT) Maio 2015
7540 Hypertext Transfer Protocol Version 2 (HTTP/2) Maio 2015
7797 JSON Web Signature (JWS) Unencoded Payload Option Fevereiro 2016
8725 JSON Web Token Best Current Practices Fevereiro 2020
8740 Using TLS 1.3 with HTTP/2 Fevereiro 2020
Draft Hypertext Transfer Protocol Version 3 (HTTP/3) Outubro 2020

Apesar de todas as RFCS serem relativamente recentes, o HTTP tem sido usado pela World-Wide Web desde 1990, sendo o formato HTTP/1.1 o mais conhecido. As atualizações propostas pelas versões mais recentes não alteraram a estrutura do protocolo, somente a codificação das mensagens trocadas. Os itens da sua estrutura, como Métodos, Caminhos, Cabeçalhos e Corpo da Mensagem, continuam existindo.

A grande vantagem do HTTP/1.1 é que o mesmo é um protocolo humanamente legível. Você pode abrir uma sessão telnet escrever uma requisição HTTP com uma linha.

$ telnet www.w3.org 80
Trying 128.30.52.100...
Connected to www.w3.org.
Escape character is '^]'.
GET http://www.w3.org/pub/WWW/TheProject.html HTTP/1.1

HTTP/1.1 400 Bad Request
date: Wed, 02 Dec 2020 17:59:53 GMT
last-modified: Thu, 04 Jun 2020 15:34:04 GMT
etag: "420-5a743dfdcf300"
accept-ranges: bytes
content-length: 1056
vary: upgrade-insecure-requests
content-type: text/html; charset=iso-8859-1
x-backend: www-mirrors

<!DOCTYPE html>
(...)
</html>


Connection closed by foreign host.

Histórico

Inicialmente o HTTP era usado somente para visualização de conteúdo nos navegadores. As páginas Web tinham poucas funcionalidades e seu conteúdo era renderizado pelo servidor. Depois os navegadores começaram a ter mais funcionalidades graças ao Javascript. Com o advento de uma linguagem a ser executado no navegador, era possível alterar a DOM, esta é o modo de acesso de todos os elementos da UI via código. Cada elemento HTML era um Objeto Javascript, podendo ser alterado. Isso deu vida ao que chamamos Web 2.0, as páginas estaticas evoluiram para um versão mais interativa. Cada usuário poderia ter contas de acesso e o conteúdo da página atualizava de acordo com a iteração do usuário, as vezes sem fazer requisição HTTP. Vale lembrar que nessa época uma Requisição HTTP as vezes era muito demorada.

Tudo isso impulsionou a requisição AJAX, onde dentro de um código Javascript era feito uma requisição HTTP, não estamos falando de REST, muitas vezes era feita uma requisição de uma outra página HTML ou de um XML, ou era acessado um serviço via SOAP. Só pra lembrar, os navegadores tinham suporte a SOAP, mas muitas vezes ele era feito diretamente por AJAX, porque o grande problema da época era a compatibilidade dos navegadores.

Paralelo ao crescimento das funcionalidades do lado do cliente, surgiu os Smartphones. Ambos de início eram muito rudimentares. As apliações Web, se é que podemos chamar assim, era construida basicamente usando jQuery. A grande vantagem do jQuery era criar uma fachada para acesso ao navegador, tentando assim diminuir a dor de portar uma aplicação em todos os navegadores do mercado. Assim estas aplicações foram evoluindo até surgirem as primeiras Single-Page Applications (SPA) e os primeiros frameworks Frontend.

As principais forças para o crescimento das APIs foram as SPAs e os aplicativos Mobile. Os microserviços acabaram se popularizando na mesma época, mas grande parte das APIs são feitas para serem consumida como serviços a serem exibidos diretamente para usuários.

Implementação Java

Java já tem mais de 25 anos, quantas vezes você imagina que o protocolo HTTP já foi implementado na linguagem? Incontáveis! Eu mesmo já implementei um como prova de conceito antes de conhecer e saber usar as especificações do Jakarta EE (previamente conhecido como Java EE). A grande vantagem de Java sobre outras linguagens são as especificações, você não precisa conhecer as implementações de cada framework HTTP, basta conhecer uma das especificações JSR 370: JavaTM API for RESTful Web Services (JAX-RS 2.1) Specification ou JSR 340: Java Servlet 3.1 Specification e escolher qual implementação usar.

A diferença básica entre essas duas especificações é o escopo de cada uma. Enquanto a Java Servlet é focada em responder a requisição HTTP, a JAX-RS é focada em prover uma API REST. Assim a JAX-RS tem como base ser:

Aqui apresentarei como usar o JAX-RS, visto que hoje ele é o mais conhecido e usado no mercado. Isso não significa que o Servlet é ultrapassado, muito pelo contrário, ele é a base para o JAX-RS. Quando o assunto apresentado fugir do escopo do JAX-RS, será apresentado a especificação que implementa a solução. Para microsserviços, temos um conjunto de especificações conhecidos como MicroProfile.io, eles buscam trazer implementações de padrões de serviços, esses não são definidos pelo Protocolo HTTP, mas são padrões de mercado recomendados.

Elementos do Protocolo

O Protocolo HTTP é de certa forma simples, por ser stateless, não há vários estados ou tipos de requisição. Podemos resumir o protocolo na análise simples de uma requisição. Vamos começar vendo os elementos proposto na versão 1.1, para depois vermos o que foi adicionado nas versões mais recentes.

Podemos dizer que uma Requisição HTTP terá os seguintes elementos: Método, URI, Cabeçalhos e Corpo.

Elemento Descrição
Método Indica o que deve ser feito ao recurso. Os métodos existente são: OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE e CONNECT.
URI Indica para qual recurso a Requisição deve ser aplicada.
Cabeçalho Informações adicionais a Requisição passada pelo cliente. Atuam como modificadores da requisição. Os cabeçalhos definido pelo protocolo são: Accept, Accept-Charset, Accept-Encoding, Accept-Language, Authorization, Expect, From, Host, If-Match, If-Modified-Since, If-None-Match, If-Range, If-Unmodified-Since, Max-Forwards, Proxy-Authorization, Range, Referer, TE e User-Agent.
Corpo Entidade associada a Requisição. Só existe na requisição associada aos métodos: POST e PUT.

Para cada Requisição existe uma Resposta e esta tem os mesmo elementos.

Elemento Descrição
Status Composto por um código com 3 dígitos e descrição textual dele. Esses código são agrupados por centenas.
Cabeçalho Informações adicionais sobre a Resposta.
Corpo Entidade respondida pelo servidor. Não é obrigatório, mas qualquer Método aceita corpo na resposta

Vale ressaltar que no HTTP/1.1 esse protocolo é legível para nós, a separação de cada elemento se dá usando CRLF, que são os caracteres Carriage Return (13) + Line Feed (10).

Todos esses elementos são importantes e todos são usados em qualquer servidor. Muitos problemas ocorrem por causa de Código de Status sendo usado incorretamente, ou Cabeaçalhos sendo ignorados. Analisaremos cada elemento baseado em Casos de Uso.

  1. Decidindo o Recurso
  2. Decidindo a Ação
  3. Decidindo o Formato
  4. Apresentando as credenciais
  5. Colocando Valores na Requisição
  6. Adicionando Parâmetros de busca
  7. Escrevendo a Resposta
  8. Informando o tipo de Resposta
  9. Tratando erros
  10. Usando o Cache

1. Decidindo o Recurso

Na Requisição HTTP, a URI tem como objetivo identificar o Recurso a ser objeto da Requisição. Em muitos contextos a URI será chamada de Caminho, Path, ou Endpoint.

Vamos analisar a API do Twitter, se selecionarmos as opções Search Tweets e Get Tweet timelines, vamos observar que há uma pequena diferença na URL das duas:

Observe que elas tem uma raiz em comum https://api.twitter.com/1.1. O que está acontecendo? O Twitter hospeda todas a suas API sob o DNS api.twitter.com e usando HTTPS, que é HTTP sob uma camada de criptografia, como visto na imagem abaixo. A URI contém alguns nós, o primeiro dele 1.1 se refere a versão da API, provavelmente é usado por um servidor que não responderá a requisição, apenas fará o proxy dela para outro servidor contendo a implementação dessa versão. Dado os dois endpoints, provavelmente a mesma implementação do servidor irá receber requisições com URIs /search/tweets.json e /statuses/home_timeline.json.

Informações de Segurança

Podemos verificar qual o endereço físico que resolve api.twitter.com usando um simples ping.

$ ping api.twitter.com

Pinging tpop-api.twitter.com [104.244.42.194] with 32 bytes of data:
Reply from 104.244.42.194: bytes=32 time=162ms TTL=54
Reply from 104.244.42.194: bytes=32 time=214ms TTL=54
Reply from 104.244.42.194: bytes=32 time=156ms TTL=54
Reply from 104.244.42.194: bytes=32 time=166ms TTL=54

Ping statistics for 104.244.42.194:
    Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
    Minimum = 156ms, Maximum = 214ms, Average = 174ms

Muito provavelmente uma máquina com om endereço 104.244.42.194 vai receber uma requisição com URL /1.1/search/tweets.json. Esta máquina vai copiar essa requisição e enviará para outro processo ou máquina com a URI /search/tweets.json.

Implementação usando JAX-RS

Com JAX-RS é possível criar classes para Recursos (Resources). Assim cara endpoint seria mapeado para uma classe separada. Vamos imaginar então que existiria uma classe para o recurso Search e outra para Statuses. Como o recurso Search tem só um nó filho, vou usar como exemplo o Statuses, este tem os seguintes nós filhos:

Todos esses podem ser criados dentro da classe Resource StatusResource. Para resolver a URI corretamente, podemos dizer que a classe irá resolver todas as URIs começada com /statuses usando a Annotation javax.ws.rs.Path, e nessa classe vamos criar os métodos para cada recurso e métodos usando a mesma Annotation e a especifica do Método javax.ws.rs.GET.

@Path("/statuses")
public class StatusResource {
    @GET
    @Path("/home_timeline")
    public List<Tweet> getHomeTimeline() {
        // some code
    }

    @GET
    @Path("/mentions_timeline")
    public List<Tweet> getMentionsTimeline() {
        // some code
    }

    @GET
    @Path("/user_timeline")
    public List<Tweet> getUserTimeline() {
        // some code
    }
}

2. Decidindo a Ação

Apesar de ignorado por alguns protocolos, o Método HTTP é muito importante, ele dá uma nova dimensão a requisição. podemos por exemplo para um mesmo recurso ter várias ações diferentes. O protocolo foi pensado para prover acesso a recursos através da internet, assim cada método significa uma operação em um recurso. Vimos anteriormente que a URI define qual recurso será acessado, e o Método define qual operação será feita.

Protocolos como SOAP tentam trazer um nível de abstração usando o corpo da requisição, desse modo o método usado deve ser sempre o POST. Mas APIs REST se baseiam muito no Método, ele é um dos pontos centrais da API. Normalmente em servidor temos apenas os Métodos GET para recursos como arquivos Javascript e páginas HTML. Alguns mais antigos implementam o POST para formulários. Foi a API REST que trouxe os Métodos como elemento primordial para o protocolo, ela dá significância a POST, PUT, PATCH e DELETE.

Na tabela abaixo são listadas as definições de cada Método pela RFC 2616.

Método Definição É Idempotente?
OPTIONS O método OPTIONS representa um pedido de informação sobre as opções de comunicação disponíveis na cadeia de pedido / resposta identificada pelo Request-URI. Sim
GET O método GET significa recuperar qualquer informação (na forma de uma entidade) é identificada pelo Request-URI. Sim
HEAD O método HEAD é idêntico ao GET, exceto que o servidor NÃO DEVE retornar um corpo de mensagem na resposta. Sim
POST O método POST é usado para solicitar que o servidor de origem aceite a entidade incluída na solicitação como um novo subordinado do recurso identificado pelo Request-URI na Request-Line. Não
PUT O método PUT solicita que a entidade incluída seja armazenada no URI de Solicitação fornecido. Sim
DELETE O método DELETE solicita que o servidor de origem exclua o recurso identificado pelo Request-URI. Sim
TRACE O método TRACE é usado para invocar um loopback remoto da camada de aplicativo da mensagem de solicitação. Sim
CONNECT Esta especificação reserva o nome do método CONNECT para uso com um proxy que pode mudar dinamicamente para ser um túnel (por exemplo, túnel SSL). N/A

Observe que o PATCH não está listado. Ele havia sido proposto pela RFC anterior RFC 2068 Hypertext Transfer Protocol – HTTP/1.1, mas como não havia uma implementação dele, foi removido na atualização. Mas vale lembrar que segundo a própria especificação do protocolo, o Método é um valor em aberto, podendo ser usando qualquer token.

Para APIs REST, os Métodos ganharam outra significância. As vezes são chamados de Verbos, como podemos ver no Guideline para APIs REST da Microsoft. Eles descrevem cada Verbo a ser utilizado da seguinte forma:

Método Descrição É Idempotente?
GET Retorna o valor atual de um objeto. Sim
PUT Sobrescreve um objeto, ou cria um objeto nomeado, quando aplicável. Sim
DELETE Remove um objeto. Sim
POST Cria um novo objeto baseado nos dados fornecidos, ou enviados pelo comando. Não
HEAD Retorna os metadados de um objeto para a resposta de GET. Recursos que suportam GET podem suportar também HEAD. Sim
PATCH Aplica uma atualização parcial a um objeto Não
OPTIONS Retorna informação sobre a requisião. Permite que um cliente recupere informações sobre um recurso, no mínimo, retornando o cabeçalho Allow denotando os Métodos válidos para este recurso. Sim

Implementação usando JAX-RS

Para demonstrarmos como implementar em JAX-RS, vamos partir do pressuposto que precisamos criar uma API de CRUD de usuários. Essa API deve ter as opções de Criar, Ler, Alterar e Apagar. Também vamos adicionar uma opção de metadados, que retornará todas as estatisticas de acesso do usuário.

Para isso precisamos projetar uma classe recursos chamada UserEnpoint, com alguns métodos implementando cada função. Para especificar os métodos, precisamos apenas usar as Annotations javax.ws.rs.GET, javax.ws.rs.PUT, javax.ws.rs.DELETE, javax.ws.rs.POST, javax.ws.rs.HEAD, javax.ws.rs.PATCH ou javax.ws.rs.OPTIONS.

@Path("/user")
public class UserEndpoint {

    @PUT
    public UserResponse createUser(CreateUserRequest request) {
        // some code
    }

    @GET
    public List<UserResponse> findUsers() {
        // some code
    }

    @GET
    @Path("/{userId: [1-9][0-9]*}")
    public UserResponse getUser(@PathParam("userId") int userId) {
        // some code
    }

    @POST
    @Path("/{userId: [1-9][0-9]*}")
    public UserResponse updateUser(@PathParam("userId") int userId, UpdateUserRequest request) {
        // some code
    }

    @PATCH
    @Path("/{userId: [1-9][0-9]*}")
    public UserResponse updateUserProperties(@PathParam("userId") int userId, UpdateUserRequest request) {
        // some code
    }

    @DELETE
    @Path("/{userId: [1-9][0-9]*}")
    public UserResponse deleteUser(@PathParam("userId") int userId) {
        // some code
    }

    @HEAD
    @Path("/{userId: [1-9][0-9]*}")
    public UserStatisticsResponse getUserStatistics(@PathParam("userId") int userId) {
        // some code
    }

}

Observe que usando apenas Annotations é simples e legível criar classes que serão uma fachada de acesso para API. Toda a lógica de negocios fica separada da lógica de acesso do HTTP. Nesse exemplo, acabamos criando caminhos usando regras e variáveis, mas isso serão visto em outro caso de uso.

3. Decidindo o Formato

Através dos cabeçalho Accept, Accept-Language, Accept-Encoding e Accept-Charset o cliente e o servidor podem negociar o que será enviado como resposta.

Accept: application/json, text/plain, */*
Accept-Language: en,en-US;q=0.8,pt-BR;q=0.5,pt;q=0.3
Accept-Encoding: gzip, deflate
Accept-Charset: iso-8859-5, unicode-1-1;q=0.8

Para entender o que a requisição acima está pedindo, precisamos entender um pouco como funciona esse campo. Começaremos com o Accept-Language, mas a interpretação do conteúdo vale pra todos, só mudando o escopo.

Na requisição, o navegador envia todas os idiomas que o usuário configurou com seus respectivos pesos. No caso tempos que o usuário aceita os idiomas nas ordens de prioridade:

  1. Inglês → en
  2. Inglês Americano → en-US
  3. Português Brasileiro → pt-BR
  4. Português → pt

Como que a ordem de prioridade é definida? Através do parâmetro q=0.3. Parece estranho porque intuitivamente você entendeu que o separador é o ;, mas não o separador é o ,. O ; esta como separador entre valor e parâmetro. Quando omitido, o valor de q, relative quality factor ou fator de qualidade relativa ou simplemente peso, é 1.0.

Com esses valores, o servidor vai decidir como será processada a resposta. Se estiver acessando um conteúdo disponível em inglês ou português, vai escolher por português. Se o conteúdo da resposta puder ser exibido como JSON ou XML, escolherá JSON.

Cabeçalho Escopo
Accept Especifica os tipos de mídia aceitos como resposta.
Accept-Language Especifica quais idiomas são aceitos como resposta.
Accept-Encoding Especifica como a reposta pode ser codificada.
Accept-Charset Especifica qual charset são aceitos como resposta.

Implementação usando JAX-RS

O JAX-RS dá suporte para controlarmos quais serão os formatos consumidos/produzidos pela API desenvolvida. Para isso tem que se usar as Annotations javax.ws.rs.Consumes e javax.ws.rs.Produces.

@POST
@Path("/")
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public UserUpdatedResponse update(UserUpdateRequest request) {
    // some code
}

4. Apresentando as credenciais

Como já foi dito, o protocolo HTTP é stateless. Não há como se guardar o estado da conexão no protocolo. Cada requisição é única e o protocolo não tem a intenção de se manter o estado. Então como é feito a identificação de usuário e da sessão? Para responder essa pergunta temos que olhar para os Cabeçalhos!

A RFC 2616 define o cabeçalho Authorization com essa finalidade, ele deve conter a identificação do usuário. Vale lembrar que, se falando de cabeçalhos, o protocolo não determina como as coisas devem ser feitas, apenas dá um direcionamento. Essa RCF não determina como o Authorization deve ser usado, usando apenas o termo vago “credenciais”.

Mesmo sem ter nenhum padrão definido como obrigatório no protocolo, há um método de Autenticação padrão definido na RFC 2617. Conhecida como HTTP Basic Authentication, ela consiste no uso da tupla usuário:senha codificada em Base64. Assim o cabeçalho ficaria da seguinte forma: Authorization: Basic Base64(usuário:senha). Como a senha é apenas códificada, e não criptografada, esse método de authenticação apresenta algumas falhas de segurança se usada com HTTP. Para resolver esse problema, é aconselhável usar HTTPS adicionando uma camada de segurança a comunicação.

A RFC ainda levanta algumas falhas de segurança em authenticações HTTP, uma dessas preocupações é referente ao ataque Man in the Middle. Ela acontece quando, entre Alice e Bob, há um interlocutor observando o valor de Autorization. Com o conhecimento desse valor, ele pode ser reusado em outras requisições. Assim quando Alice envia uma mensagem para Bob, o interlocutor pode ler o valor e fingir ser Alice.

Além da Autenticação Basic, há outros modos. Uma forma bem comum é o uso de Cookies. Eles não são uma boa prática e muitas vezes são usados de maneira antiética por empresas conhecidas como Big Techs. Um Cookie consiste em uma informação adicionada no cliente ou no servidor que será replicada a cada iteração. Definido na RFC 6265, o Cookie é um mecanismo para armazenar Tokens, esses Tokens podem servir para armazenar informações do usuário. Assim uma informação simples como Cookie: SID=31d4d96e407aad42; lang=en-US pode conter todo o registro de acesso do usuário em vários endereços da internet. Vale lembrar que o Cookie é replicado apenas por um endereço, mas hoje temos códigos Javascript de várias empresas rodando nos nossos sites e coletando Cookies.

O Cookie também é usado criar o registro da sessão do usuário, pois ele é uma solução simple onde há um método de expiração do Token. Assim, ao se logar em um servidor e este usar o Set-Cookie: session=e1d8ade7aa00f1a7e66f6f324a5819ad; domain=.mycompany.com; path=/; expires=Fri, 18 Dec 2020 10:37:38 GMT;, temos um acesso que irá expirar em 18 de dezembro de 2020. Se o usuário não acessar o servidor novamente até essa data, será necessário um novo Token.

Com a evolução dos frameworks frontend, um token não era mais suficiente para o registro da sessão. Ele é muito útil quando temos todas as decições tomadas apenas de um lado da comunicação. Mas e quando são necessárias informações de acesso dos dois lados? Para isso a RFC 7519 define o JWT, que é um formato para armazenar informações do usuário. Usando criptografia assimétrica o token é gerado por uma autoridade, que não necessariamente precisa ser o servidor para qual está sendo enviada a requisição. Esse pode ser usado no cabeçalho Autorization como token método Bearer, definido nas RFC 6749.

Para exemplificar o JWT, vamos imaginar o cabeçalho abaixo, qualquer cliente pode ler esse token e descobrir que ele usa o algoritmo HS256 e se refere ao usuário John Doe com identificador 1234567890 possuindo as permissões de ADMIN e EDITOR. Um token JWT é composto de 3 partes, a primeira contem o o tipo do token e o algoritmo usado para criptografar o token. Já a segunda parte conterá um objeto JSON com todas as informações do usuário, alguns campos desse JSON são definidos pela RFC 7919, mas a aplicação pode adicionar o que for necessário. E o último token serve para validar a autenticidade do token, ele vai conter o conteúdo das duas primeiras sessões encriptada com a chave privada que não deve ser compartilhada. Cada serviço que contenha a chave pública pode ler essa informação e validar esse token foi gerado pela autoridade certificador, ou seja, o nosso serviço de authenticação de usuários.

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJBRE1JTiIsIkVESVRPUiJdfQ.ppRwCyy0PiXgtfRgMZm77CIWHq-Y6sUFtxM2vahlv-k

JWT decodificado em jwt.io

Implementação usando Microprofile.io

Como há muitos métodos de autenticação, vamos apresentar como o proposto pela especificação JWT RBAC for Microprofile. Essa especificação usa Annotations definidas na JSR-000250 Common Annotations for the JavaTM Platform 1.0.

Para o nosso exemplo, vamos imaginar um serviço de acesso a usuário onde cada usuário pode pegar suas proprias informações, mas apenas usuários Administradores pode listar usuários. Qualquer usuário poderá fazer o login quando não autenticado. Para permitir o acesso de usuário não autenticados, devemos usar a Annotation javax.annotation.security.PermitAll. Para restringir o acesso a usuários autenticados, devemos usar javax.annotation.security.DenyAll. E por fim, para restringir o acesso apenas para alguns usuários com certas permissões, devemos definir quais são as funções permitidas usando javax.annotation.security.RolesAllowed.

Vale lembrar, que de acordo com a implementação, alguns vão requerer que todas as funções sejam declaradas usando javax.annotation.security.DeclareRoles.

Para na lógica de validação da requisição, o MicroProfile Auth prevê dois métodos de retorno dos valores do JWT. Podemos injetar usando CDI todos os valores, ou apenas um Claim do JWT. Para acessar todo token, precisamo usar a classe org.eclipse.microprofile.jwt.JsonWebToken. Já para acessar um Claim especifico, só usar a Annotation org.eclipse.microprofile.jwt.Claim como qualifier.

Assim, se criamos a classe UserEndpoint, vamos ter:

@Path("/user")
@DeclareRoles({ "ADMIN" })
public class UserEndpoint {

    @Inject
    private JsonWebToken callerPrincipal;

    @Inject
    @Claim(value="exp", standard=Claims.iat)
    private Long timeClaim

    @GET
    @RolesAllowed({ "ADMIN" })
    public List<UserResponse> findUsers() {
        // some code
    }

    @GET
    @Path("/me")
    @DenyAll
    public UserResponse getLoggedUser() {
        // some code
    }

    @POST
    @Path("/login")
    @PermitAll
    public UserResponse doLogin(LoginRequest request) {
        // some code
    }

}

5. Colocando Valores na Requisição

Algumas requisições tem um corpo, a definição do protocolo não se preocupa em definir quais Métodos deve ou não ter um corpo associado a mensagem. Mas já é um padrão que somente o POST e PUT tem um body associado, tanto que as especificações do 3GPP para 5G coloca que somente operações de remoção quando não requerem parametros que não identifiquem o recurso devem usar DELETE (4.2.3 TS 129 501 V15.7.0). Isso significa que operações como /chargingdata/{ChargingDataRef}/release do module CHF - Converged Charging devem usar POST, por requerem mais informação que um identificador.

Por ser um protocolo bem simples, o HTTP aceita qualquer coisa no corpo da mensagem. Este só é finalizado quando há dois CRLF seguindos, mas sempre é recomendado o uso do cabeçalho Content-Length para evitar que o conteúdo da mensagem seja confundindo com a finalização da mensagem.

Esse valor não é interpretado pelo protocolo, essa função é da aplicação. Assim, não há problema haver discrepância de dados entre o cabeçalho e o corpo da mensagem. Mas vale lembrar que tudo funcionará melhor se o conteúdo vier associado aos cabeçalhos Content-Type e Content-Length.

Content-Type: application/x-www-form-urlencoded
Content-Length: 115

O valor de Content-Type vai informar a aplicação como o dado no corpo da mensagem deve ser interpretado, este é um MIME Type. Assim um valor pode ser serializado tanto em XML quanto em JSON, mas se esse formato estiver especificado no Content-Type a aplicação poderá ler ele sem nenhum problema.

Implementação usando JAX-RS

Para adicionar um corpo na mensagem do JAX-RS, não é preciso nenhuma configuração especial. O método que implementa esse endpoint deve conter a Annotation javax.ws.rs.Consumes e deve ser um javax.ws.rs.POST ou javax.ws.rs.PUT. A única especificidade é que o método deve ter um parâmetro que é um POJO. Abaixo segue o exemplo que já usamos anteriormente.

@POST
@Path("/")
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public UserUpdatedResponse update(UserUpdateRequest request) {
    // some code
}

6. Adicionando Parâmetros de busca

Se olharmos bem para o Path, veremos que a URI tem uma porção que é comum para vários recursos. Chamamos de Query String tudo que vem depois de um ?. O recurso é identificado pelo caminho que vai do inicio até o ?, o que vem depois é apenas um parâmetro. Vamos analisar o caso abaixo, nesse caso /a/b/c é o recurso com dois parâmetros que alteram essa busca.

/a/b/c?key1=value1&key2=value2

Query Strings devem ser usado com muita parcimonia, pois em muitos casos esses valores devem vir no corpo da mensagem e não na requisição. Seu uso deve ser limitado a buscas, como o proprio nome já diz, e isso já limita para o Método GET.

Implementação usando JAX-RS

No JAX-RS, como ele é um parâmetro, deve ser declarado como um parâmetro da função. Para isso deve-se usar a Annotation javax.ws.rs.QueryParam. Quando há valores padrão, deve-se usar a Annotation javax.ws.rs.DefaultValue passando o valor padrão como uma String.

Vamos imaginar como exemplo, um serviço de busca de usuário que pode aceitar três parâmetros de busca: nome, idade e ativo. Todos os parâmetros são opcionais, mas se ativo não for provido deve retornar apenas os usuários ativos no sistema. Para resolver esse problema, vamos criar os 3 parâmetros usando QueryParam, mas somente ativo terá DefaultValue. Quando não existir a chave na URL, o valor respectivo vai vir null, por isso usei o objeto Integer e não o primitivo para a idade. Mas como estou provendo o valor de ativo, posso usar o tipo primitivo.

@GET
@Path("/")
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public List<User> search(@QueryParam("name") String name, 
                         @QueryParam("age") Integer age, 
                         @DefaultValue("true") @QueryParam("active") boolean active) {
    // some code
}

7. Escrevendo a Resposta

O valor retornado como resposta não é obrigatório, a reposta pode vir de outras formas. Mas por padrão ela acontece exatamente igual ao corpo da requisição. São usados os mesmo cabeçalhos: Content-Type e Content-Length. A única diferencia é que o Content-Type é decidido pelo servidor de acordo com o valor de Accept.

Implementação usando JAX-RS

Para adicionar um corpo na mensagem de resposta do JAX-RS, não é preciso nenhuma configuração especial. O método que implementa esse endpoint deve conter a Annotation javax.ws.rs.Produces. A única especificidade é que o método deve retornar um POJO. Abaixo segue o exemplo que já usamos anteriormente.

@POST
@Path("/")
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public UserUpdatedResponse update(UserUpdateRequest request) {
    // some code
}

8. Informando o tipo de Resposta

Um ponto muito importante que ainda não foi explicado é o Código de Estado. Cada resposta HTTP tem um código especifico e, muitas vezes, ele diz mais sobre a resposta do que a propria resposta. Os códigos são números inteiros no intervalo [100,599]. Cada centena tem um significado especifico.

Intervalo Tipe Descrição
1xx Informacional Requisição recebida, continuando o processo…
2xx Sucesso A ação foi recebida, compreendida e aceita com sucesso.
3xx Redirecionamento Outras ações devem ser tomadas para concluir a solicitação.
4xx Erro do Cliente A solicitação contém sintaxe incorreta ou não pode ser atendida.
5xx Erro do Servidor O servidor falhou em cumprir um pedido aparentemente válido.

É muito importante conhecer cada mensagem, pois muitas vezes um erro pode ser feito a usar a mensagem errada. Há questões como, se uma busca é feita e não se encontra resultado nenhum, deve se retornar 404? Obviamente que não, pois a busca foi concluida com sucesso, é mais importante retornar uma lista vazia. O 404 deve ser retornado se ou não uma ação mapeada para o recurso, ou se a entidade procurada não é encontrada. Há erros como ao se fazer uma alteração o cliente verifica por um resultado 200, mas o servidor retorna 201 ou 204.

Há erros que são muito comuns e fazem parte do ciclo da operação. Podemos citar o 409, quando há um conflito e a operação ter que ser refeita completamente. Ou o 401 quando a Autenticação expirou.

Implementação usando JAX-RS

Uma das limitações do JAX-RS é que ele é voltado para POJOs e não há uma maneira fácil de retornar o POJO e o código de estado da resposta. Para fazer isso é preciso instanciar um objeto javax.ws.rs.core.Response.

@POST
@Path("/")
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Response update(UserUpdateRequest request) {
    // some code
    return Response.ok() // 200
                   .entity(response)
                   .build();
}

@POST
@Path("/")
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Response create(UserCreateRequest request) {
    // some code
    return Response.status(Status.CREATED) // 201
                   .entity(response)
                   .build();
}

9. Tratando erros

O tratamento de erro é a continuação do tópico anterior. Em HTTP os erros são bem definidos e existem código de erros especificos. Imagina o caso de que um parâmetro veio no formato incorreto, ou está faltando um parâmetro obrigatório? Esse caso deve retornar 400. Uma grande vantagem do HTTP é que os códigos de estado podem ser criados respeitando a separação pode tipo. Então se sua API quer criar um novo tipo de erro, que seja especifico do negócio, só colocar um código entre 450 e 499.

Retornar uma mensagem de erro não significa que a mensagem não pode ter uma resposta. É uma boa pratica em APIs explicar o erro. Assim a resposta pode ter um razão que vai além do código do estado. É preciso muito cuidado para não expor detalhes da implementação como Stack Traces.

{
    "status": 400,
    "message": "Parameter userId is required!"
}

Implementação usando JAX-RS

Para um tratamento de erros ótimo, são necessários duas coisas. Primeiro deve-se gerar uma Exception no momento em que o erro ocorre, e depois essa Exception deve ser trata para se tornar uma resposta.

Exception Descrição
BadRequestException A runtime exception indicating a bad client request.
ClientErrorException A base runtime application exception indicating a client request error (HTTP 4xx status codes).
ForbiddenException A runtime exception indicating that an access to a resource requested by a client has been forbidden by the server.
InternalServerErrorException A runtime exception indicating an internal server error.
NotAcceptableException A runtime exception indicating that a client request is not acceptable by the server.
NotAllowedException A runtime exception indicating a client requesting a resource method that is not allowed.
NotAuthorizedException A runtime exception indicating request authorization failure caused by one of the following scenarios: a client did not send the required authorization credentials to access the requested resource, i.e.
NotFoundException A runtime exception indicating a resource requested by a client was not found on the server.
NotSupportedException A runtime exception indicating that the client request entity media type is not supported.
ProcessingException A base runtime processing exception.
RedirectionException A runtime application exception indicating a request redirection (HTTP 3xx status codes).
ServerErrorException A base runtime application exception indicating a server error (HTTP 5xx status codes).
ServiceUnavailableException A runtime exception indicating that the requested resource cannot be served.
WebApplicationException Runtime exception for applications.

O próximo passo é criar um javax.ws.rs.ext.ExceptionMapper usando a Annotation javax.ws.rs.ext.Provider. Essa deve criar uma Response baseada na Exception.

@Provider 
public class ErrorMessageMapper implements ExceptionMapper<WebApplicationException> { 

    @Override 
    public Response toResponse(WebApplicationException exception) { 
        // some code
    } 

} 

10. Usando o Cache

Como último caso de uso, deixei o uso de Cache. Em alguns casos, qualquer processamento é caro ao servidor, mesmo que seja só para retornar informações. Muitos softwares distribuidos escalam e, quando falamos de escalabilidade, qualquer economia de tempo ou memória é importante assim se houver a possibilidade de um servidor não precisar responder uma requisição, ele poderá usar esse tempo respondendo outra.

O Cache de uma requisição pode ser feito por vários estágios, mas para termos em mente como é feito é preciso termos em mente que para um serviço de alta disponibilidade nem sempre existe só cliente e servidor. Muitas vezes há Servidores de Balanceamento de Carga e Servidores de Cache, há também a base de dados. Quando mais profunda a requisição chegar, mais será usado da infraestrutura.

HTTP Em larga escala

Podemos classificar também os recursos como de dois tipos. Os Recursos Estáticos são aqueles que não podem ser alterados durante o ciclo de vida da aplicação, estamos falando de arquivos Javascript ou CSS, Imagens. Muitos deles também não precisam ter um nome legível, podendo assim associar seu nome ao seu conteúdo. Podemos também falar de Recursos Dinâmicos, eles são alterados durante o ciclo de vida de uma aplicação. O autor de blog pode fazer uma correção no texto. Um usuário pode ter seus dados alterados.

Para Recursos Estáticos, a melhor forma de fazer o check é usar o cabeçalho Cache-Control, com ele você pode definir se um recurso vai ter cache ou não, se ele não vai mudar e qual a idade máxima do mesmo. Tem que se ter muito cuidado ao usar ele se o recurso for mutável. Talvez você tá tenha experimentado erros em um página e ao fazer refresh (CTRL + F5) o mesmo voltou a funcionar, isso acontecia antes dos frameworks frontend porque alterações em arquivos não eram refletidas no nome do arquivo. Porque isso não acontece mais? Porque os frameworks inserem no nome do recurso um HASH do conteúdo do mesmo, e muitas vezes o conteúdo de um arquivo Javascript é gerado a partir de uma série de outros recursos.

A resposta com Cache-Control pode ser feita usando o Código de Estado 304 - Not Modified, ou em alguns casos o cliente nem chega a enviar a requisão. Essa resposta 304 pode ser produzida tanto pelo próprio servidor ou por algum servidor de cache.

A figura abaixo mostra um exemplo retirado de um caso concreto. Na primeira vez que vai fazer a requisição, o cliente não envia o valor de If-None-Match. Mas da segunda, por causa da resposta do servidor é enviado. Na segunda resposta o servidor não respondeu os dados da mensagem, reduzindo o uso da rede.

Diagrama de Sequência

Para finalizar, vou apresentar soluções prontas de caching que podem ser usadas. Uma delas é o NGINX, ele serve tanto como cache como para balanceamento de Carga. Vale ressaltar que se você usa Kubernetes, isso já faz parte da sua infraestrutura naturalmente.

Implementação usando JAX-RS

A implementação de qualquer mecanismo de cache nunca é simples. Não vamos aqui entrar em detalhes de como ela pode ser feita, o foco vai ser o suporte dado pela especificação JAX-RS a ela.

Segundo a espeficicação, em cada requisição temos o objeto javax.ws.rs.core.Request associado que pode ser passado como parâmetro, se acompanhado da Annotation javax.ws.rs.core.Context. Com o objeto Request, temos o método evaluatePreconditions que consegue validar se a data de última modificação é valida ou se a ETag é a mesma. Como você vai resolver essa data? Isso é um futuro problema!

@GET
public Response getUsers(@Context Request request) {
    Date lastModifiedDate = // retrieve last modified date for all users
    Response.ResponseBuilder responseBuilder = request.evaluatePreconditions(lastModifiedDate);
    if (Objects.isNull(responseBuilder)) {//last modified date didn't match, send new content
        return Response.ok(/* user list */)
                       .lastModified(lastModifiedDate)
                       .build();
    } else {
        //sending 304 not modified
        return responseBuilder.build();
    }
}

@GET
@Path("{id}")
public Response getUser(@PathParam("id") String id, @Context Request request) {
    EntityTag eTag = // retrieve ETag for userId = id
    Response.ResponseBuilder responseBuilder = request.evaluatePreconditions(eTag);
    if (Objects.isNull(responseBuilder)) {//eTag has changed, sending new content
        return Response.ok(/* user */)
                       .tag(eTag)
                       .build();
    } else {
        //sending 304 not modified
        return responseBuilder.build();
    }
}
Originally published December 05, 2020