ASP

18 mar, 2019

ASP.NET Core + JWT + Refit: consumindo uma API protegida de forma descomplicada

Publicidade

Embora funcional, o uso da classe HttpClient em projetos .NET costuma exigir um grande esforço de codificação na implementação de chamadas a APIs REST. E a complexidade tende a aumentar quando se faz necessário o uso de tokens de autenticação.

Um bom exemplo disso pode ser encontrado no seguinte exemplo:

Observando a classe Program deste projeto:

using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using CargaProdutos.Models;
using CargaProdutos.Clients;

namespace CargaProdutos
{
    class Program
    {
        private static string _urlBase;
 
        static void Main(string[] args)
        {
            var builder = new ConfigurationBuilder()
                 .SetBasePath(Directory.GetCurrentDirectory())
                 .AddJsonFile($"appsettings.json");
            var config = builder.Build();

            _urlBase = config.GetSection("APIProdutos_Access:UrlBase").Value;

            using (var client = new HttpClient())
            {
                client.BaseAddress = new Uri(_urlBase);
                client.DefaultRequestHeaders.Accept.Clear();
                client.DefaultRequestHeaders.Accept.Add(
                    new MediaTypeWithQualityHeaderValue("application/json"));

                // Envio da requisição a fim de autenticar
                // e obter o token de acesso
                HttpResponseMessage respToken = client.PostAsync(
                    "login", new StringContent(
                        JsonConvert.SerializeObject(new
                        {
                            UserID = config.GetSection("APIProdutos_Access:UserID").Value,
                            Password = config.GetSection("APIProdutos_Access:Password").Value
                        }), Encoding.UTF8, "application/json")).Result;

                string conteudo =
                    respToken.Content.ReadAsStringAsync().Result;
                Console.WriteLine(conteudo);

                if (respToken.StatusCode == HttpStatusCode.OK)
                {
                    Token token = JsonConvert.DeserializeObject<Token>(conteudo);
                    if (token.Authenticated)
                    {
                        // Associar o token aos headers do objeto
                        // do tipo HttpClient
                        client.DefaultRequestHeaders.Authorization =
                            new AuthenticationHeaderValue("Bearer", token.AccessToken);

                        var apiProdutoClient = new APIProdutoClient(client);

                        apiProdutoClient.IncluirProduto(
                            new Produto()
                            {
                                CodigoBarras = "00003",
                                Nome = "Teste Produto 03",
                                Preco = 30.33
                            });

                        apiProdutoClient.IncluirProduto(
                            new Produto()
                            {
                                CodigoBarras = "00004",
                                Nome = "Teste Produto 04",
                                Preco = 44.04
                            });

                        Console.WriteLine("Produtos cadastrados: " +
                            JsonConvert.SerializeObject(
                                apiProdutoClient.ListarProdutos()));
                    }
                }
            }

            Console.WriteLine("\nFinalizado!");
            Console.ReadKey();
        }
    }
}

E também o tipo APIProdutoClient:

using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Text;
using Newtonsoft.Json;
using CargaProdutos.Models;

namespace CargaProdutos.Clients
{
    public class  APIProdutoClient
    {
        private HttpClient _client;

        public APIProdutoClient(HttpClient client)
        {
            _client = client;
        }

        public void IncluirProduto(Produto produto)
        {
            HttpResponseMessage response = _client.PostAsync(
                "produtos", new StringContent(
                    JsonConvert.SerializeObject(produto),
                    Encoding.UTF8, "application/json")).Result;

            Console.WriteLine(
                response.Content.ReadAsStringAsync().Result);
        }

        public List<Produto> ListarProdutos()
        {
            HttpResponseMessage response = _client.GetAsync(
                "produtos").Result;

            List<Produto> resultado = null;
            if (response.StatusCode == HttpStatusCode.OK)
            {
                string conteudo = response.Content.ReadAsStringAsync().Result;
                resultado = JsonConvert.DeserializeObject<List<Produto>>(conteudo);
            }
            else
                Console.WriteLine("Token provavelmente expirado!");

            return resultado;
        }        
    }
}

É possível notar que a configuração de um objeto HttpClient requer várias linhas de código definindo o formato de dados a ser empregado na comunicação (application/json), bem como operações de serialização e desserialização de instâncias representando conjuntos de informações.

Como poderíamos, então, simplificar esse trabalho através de um código mais enxuto e que preserve as mesmas funcionalidades oferecidas pela classe HttpClient?

Uma boa resposta a essa questão é a biblioteca open source chamada Refit. Bastante conhecida dentro do desenvolvimento mobile com Xamarin (esta solução me foi apresentada por um amigo, o MVP Thiago Bertuzzi – grande referência em Xamarin no Brasil).

Essa alternativa atualmente é compatível com o .NET Standard: isso abre caminho para a utilização do Refit em projetos baseados no .NET Core e ASP.NET Core.

Na imagem a seguir temos a biblioteca Refit adicionada a um projeto .NET Core 2.2 (uma Console Application), com destaque para a compatibilidade com o .NET Standard:

Mas como a biblioteca Refit simplifica, do ponto de vista prático, a escrita de instruções invocando APIs REST? Por meio da definição de interfaces que servirão de base para a criação de objetos empregados nas chamadas remotas.

Para o exemplo apresentado anteriormente neste artigo, serão criadas a interface ILoginAPI:

using System.Threading.Tasks;
using Refit;
using CargaProdutos.Models;

namespace CargaProdutos.Interfaces
{
    public interface ILoginAPI
    {
        [Post("/login")]
        Task<Token> PostCredentials(User user);
    }
}

E a interface IProdutosAPI:

using System.Collections.Generic;
using System.Threading.Tasks;
using Refit;
using CargaProdutos.Models;

namespace CargaProdutos.Interfaces
{
    public interface IProdutosAPI
    {
        [Get("/Produtos")]
        [Headers("Authorization: Bearer")]
        Task<List<Produto>> ListarProdutos();

        [Post("/Produtos")]
        [Headers("Authorization: Bearer")]
        Task<ResultadoAPIProdutos> IncluirProduto([Body]Produto produto);
    }
}

Analisando o código que define essas duas interfaces é possível destacar:

  • A presença dos atributos Get, Post e Headers vinculados à declaração de métodos que representam chamadas a APIs REST. Nota-se ainda a utilização do atributo Body para a passagem de parâmetros que farão parte do corpo de uma mensagem
  • Atributos que correspondem a verbos HTTP (Get e Post, nestes exemplos) recebem como parâmetro o caminho relativo/rota utilizado para o envio de requisições
  • Todas as operações definidas nas interfaces fazem uso de tipos representando conjuntos de dados, o que permitirá que se dispense a codificação de instruções de serialização e desserialização onde essas estruturas forem empregadas

Na próxima listagem temos a versão refatorada da classe Program:

  • As chamadas ao método For da classe RestService (namespace Refit) receberão como parâmetros as interfaces detalhadas anteriormente, bem como o caminho base de acesso às APIs (definido no appsettings.jsonhttps://localhost:5001/api, para este exemplo – e que será concatenado ao caminho relativo indicado nos atributos Get e Post das interfaces)
  • No caso específico de IProdutosAPI, foi preciso também fornecer como parâmetro uma instância do tipo RefitSettings (namespace Refit) com o token obtido via interface ILoginAPI;
  • Ao utilizar o Refit não foi mais necessário seguir com uma implementação da classe APIProdutoClient, uma vez que os métodos declarados nas interfaces fazem uso explícito de tipos (e com isso contribuem para um código mais simples e enxuto).
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using Refit;
using CargaProdutos.Models;
using CargaProdutos.Interfaces;

namespace CargaProdutos
{
    class Program
    {
        static void Main(string[] args)
        {
            var builder = new ConfigurationBuilder()
                 .SetBasePath(Directory.GetCurrentDirectory())
                 .AddJsonFile($"appsettings.json");
            var config = builder.Build();

            string urlBase = config.GetSection("APIProdutos_Access:UrlBase").Value;

            // Envio da requisição a fim de autenticar
            // e obter o token de acesso
            var loginAPI = RestService.For<ILoginAPI>(urlBase);
            Token token = loginAPI.PostCredentials(
                new User()
                {
                    UserID = config.GetSection("APIProdutos_Access:UserID").Value,
                    Password = config.GetSection("APIProdutos_Access:Password").Value
                }).Result;
            Console.WriteLine(JsonConvert.SerializeObject(token));

            if (token.Authenticated)
            {
                // Associar o token aos headers do objeto
                // gerado via Refit
                var produtosAPI = RestService.For<IProdutosAPI>(urlBase,
                    new RefitSettings()
                    {
                        AuthorizationHeaderValueGetter = () =>
                            Task.FromResult(token.AccessToken)
                    });

                Console.WriteLine(JsonConvert.SerializeObject(
                    produtosAPI.IncluirProduto(
                        new Produto()
                        {
                            CodigoBarras = "00005",
                            Nome = "Teste Produto 05",
                            Preco = 5.05
                        }).Result));


                Console.WriteLine("Produtos cadastrados: " +
                    JsonConvert.SerializeObject(
                        produtosAPI.ListarProdutos().Result));
            }

            Console.WriteLine("\nFinalizado!");
            Console.ReadKey();
        }
    }
}

A seguir, é possível observar o projeto de testes (uma Console Application) em modo debugging, com objetos correspondentes às interfaces ILoginAPI e IProdutosAPI gerados automaticamente pelo Refit:

O resultado da execução dessa aplicação está na próxima imagem:

Os fontes desta nova versão baseada no Refit podem ser encontrados no seguinte repositório do GitHub:

Já o projeto da API REST de Produtos (e que contempla o uso de JWT) está no repositório a seguir:

Referências