Blazor WebAssembly - API com Autenticação JWT e Identity - II
Neste artigo veremos implementar a autenticação JWT em uma aplicação Blazor WebAssembly para acesso a uma Web API com Identity com EF Core usando a abordagem Code-First. |
Continuando a primeira parte do artigo iremos agora configurar o projeto Client definindo os serviços e criando os componentes.
Configurando o projeto Client : Criando os serviços e os componentes
Vamos iniciar a configuração do projeto Client incluindo o pacote Blazored.LocalStorage que iremos usar para persistir o token de autenticação a partir da API quando o usuário fizer o login.
Para isso inclua o pacote Nuget abaixo usando o comando : Install-Package ou dotnet add package
Blazored.LocalStorage" Version="4.1.2"
A seguir vamos atualizar o componente App do projeto Client para usar o componente AuthorizeRouteView ao invés do componente RouteView :
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true"> <Found Context="routeData"> <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/> </Found> <NotFound> <CascadingAuthenticationState> <LayoutView Layout="@typeof(MainLayout)"> <p>Sorry, there's nothing at this address.</p> </LayoutView> </CascadingAuthenticationState> </NotFound> </Router> |
O componente AuthorizeRouteView se comporta da mesma forma que RouteView, mas exibe apenas as páginas que o usuário esta autorizado a ver. Além usamos o componente CascadingAuthenticationState que fornece um parâmetro em cascata do tipo Task<AuthtenticationState> para todos os componentes descendentes que é usado pelo componente AuthorizeRouteView para determinar o estado de autenticação atual do usuário.
Criando um AuthenticationStateProvider customizado
Como estamos
usando o Blazor do lado do cliente, precisamos fornecer nossa própria
implementação para a classe AuthenticationStateProvider.
Como há tantas opções quando se trata de aplicativos do lado do cliente, não há
como projetar uma classe padrão que funcione para todos.
Precisamos substituir o método GetAuthenticationStateAsync.
Neste método, precisamos determinar se o usuário atual esta autenticado ou não.
Também vamos incluir alguns métodos auxiliares que usaremos para atualizar o
estado de autenticação quando o usuário efetuar login ou
logout.
Assim na raiz do projeto (ou em uma pasta separada se preferir) crie a classe ApiAuthenticationStateProvider com o código a seguir:
using Blazored.LocalStorage; using Microsoft.AspNetCore.Components.Authorization; using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Security.Claims; using System.Text.Json; using System.Threading.Tasks; namespace BlazorAppAgenda.Client public ApiAuthenticationStateProvider(HttpClient httpClient, ILocalStorageService localStorage) if (string.IsNullOrWhiteSpace(savedToken)) _httpClient.DefaultRequestHeaders.Authorization =
return new AuthenticationState(new ClaimsPrincipal( public void MarkUserAsAuthenticated(string email) var authState = Task.FromResult(new AuthenticationState(authenticatedUser)); public void MarkUserAsLoggedOut() private IEnumerable<Claim> ParseClaimsFromJwt(string jwt) keyValuePairs.TryGetValue(ClaimTypes.Role, out object roles); if (roles != null) foreach (var parsedRole in parsedRoles) keyValuePairs.Remove(ClaimTypes.Role); claims.AddRange(keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString()))); return claims; private byte[] ParseBase64WithoutPadding(string base64) |
Vamos entender o código desta classe:
O método
GetAuthenticationStateAsync é chamado pelo
componente CascadingAuthenticationState para
determinar se o usuário atual está autenticado ou não.
No código, verificamos se existe um token de autenticação no armazenamento
local. Se não houver um token no armazenamento local, retornamos um novo
AuthenticationState com um claims principal
em branco. Isso equivale a dizer que o usuário atual não está autenticado.
Se houver um token, nós o recuperamos e definimos o cabeçalho de autorização
padrão para o HttpClient. Em seguida, retornamos um
novo AuthenticationState com um novo claims
principal contendo as declarações do token. As declarações são
extraídas do token pelo método ParseClaimsFromJwt.
Este método decodifica o token e retorna as declarações contidas nele.
O método MarkUserAsAuthenticated é um método
auxiliar usado para quando um usuário efetua login. Seu único propósito é
invocar o método NotifyAuthenticationStateChanged
que dispara um evento chamado AuthenticationStateChanged.
Isso coloca em cascata o novo estado de autenticação, por meio do componente
CascadingAuthenticationState.
Como você pode esperar, MarkUserAsLoggedOut faz
quase exatamente o mesmo que o método anterior, mas quando um usuário efetua
logout.
Criando os serviço de Autenticação
Vamos agora criar o serviço de autenticação que será usado nos componentes Razor para registrar e logar os usuários.
No projeto Client crie a pasta Services e nesta pasta crie primeiro a interface IAuthService :
using BlazorAppAgenda.Shared; using System.Threading.Tasks; namespace BlazorAppAgenda.Client.Services |
Aqui definimos um contrato para implementar o Login, o Logout e o Registro de um novo usuário.
Na mesma pasta crie a classe AuthService que implementa a interface:
using BlazorAppAgenda.Shared;
using Blazored.LocalStorage;
using Microsoft.AspNetCore.Components.Authorization;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace BlazorAppAgenda.Client.Services
{
public class AuthService : IAuthService
{
private readonly HttpClient _httpClient;
private readonly AuthenticationStateProvider _authenticationStateProvider;
private readonly ILocalStorageService _localStorage;
public AuthService(HttpClient httpClient,
AuthenticationStateProvider authenticationStateProvider,
ILocalStorageService localStorage)
{
_httpClient = httpClient;
_authenticationStateProvider = authenticationStateProvider;
_localStorage = localStorage;
}
public async Task<RegisterResult> Register(RegisterModel registerModel)
{
var messageResult = await _httpClient.PostAsJsonAsync("api/account", registerModel);
var result = await messageResult.Content.ReadFromJsonAsync<RegisterResult>();
return result;
}
public async Task<LoginResult> Login(LoginModel loginModel)
{
var loginAsJson = JsonSerializer.Serialize(loginModel);
var response = await _httpClient.PostAsync("api/Login",
new StringContent(loginAsJson, Encoding.UTF8, "application/json"));
var loginResult = JsonSerializer.Deserialize<LoginResult>(await
response.Content.ReadAsStringAsync(), new JsonSerializerOptions
{ PropertyNameCaseInsensitive = true });
if (!response.IsSuccessStatusCode)
{
return loginResult;
}
await _localStorage.SetItemAsync("authToken", loginResult.Token);
((ApiAuthenticationStateProvider)_authenticationStateProvider)
.MarkUserAsAuthenticated(loginModel.Email);
_httpClient.DefaultRequestHeaders.Authorization = new
AuthenticationHeaderValue("bearer", loginResult.Token);
return loginResult;
}
public async Task Logout()
{
await _localStorage.RemoveItemAsync("authToken");
((ApiAuthenticationStateProvider)_authenticationStateProvider).MarkUserAsLoggedOut();
_httpClient.DefaultRequestHeaders.Authorization = null;
}
}
}
|
O método
Register envia o
registerModel ao controlador de contas e retorna o
RegisterResult ao chamador.
O método Login é semelhante ao método Register, ele
posta o LoginModel no controlador de login. Mas
quando um resultado bem-sucedido é retornado, ele remove o
token de autenticação e o persiste no armazenamento local.
Em seguida, ele chama o método MarkUserAsAuthenticated
que acabamos de criar na classe
ApiAuthenticationStateProvider. Finalmente, ele define o cabeçalho de
autorização padrão no HttpClient.
O método Logout está apenas fazendo o inverso do
método Login.
Agora só nos resta criar os componentes Blazor e usar os serviços.
Criando o componente Register.razor
Agora vamos criar os componentes Blazor para definir a interface com usuário e usar os serviços e toda a infraestrutura criada para fazer a 'mágica acontecer'.
Na pasta Pages do projeto Client crie o componente Register.razor:
@page "/register" @inject IAuthService AuthService @inject NavigationManager NavigationManager <h1>Register</h1> @if
(ShowErrors) <div
class="card">
<div class="form-group"> @code {
private RegisterModel RegisterModel = new RegisterModel();
private async Task HandleRegistration() var result = await AuthService.Register(RegisterModel);
if (result.Successful) |
Este componente contém um formulário que permite ao usuário inserir seu endereço de e-mail e a senha desejada. Quando o formulário é submetido, o método Register no AuthService é chamado de passando o RegisterModel. Se o resultado do registro for um sucesso, o usuário é direcionando para a página de login. Caso contrário, quaisquer erros são exibidos para o usuário.
Criando o componente Login.razor
O componente Login.razor será criado na mesma pasta Pages e será responsável por autenticar o usuário existente.
@page "/login" @inject IAuthService AuthService @inject NavigationManager NavigationManager <h1>Login</h1> @if (ShowErrors) <div class="card"> <div class="form-group"> @code { private LoginModel loginModel = new LoginModel(); private async Task HandleLogin() var result = await AuthService.Login(loginModel); if (result.Successful) |
Neste componente
existe um formulário para o usuário inserir seu endereço de e-mail e senha.
Quando o formulário é enviado, o AuthService é
chamado e o resultado é retornado. Se o login for bem-sucedido, o usuário será
redirecionado para a página inicial, caso contrário, será exibida a mensagem de
erro.
Criando o componente Logout.razor
Este componente permite realizar o Logout do usuário :
@page "/logout" @inject IAuthService AuthService @inject NavigationManager NavigationManager @code { protected override async Task OnInitializedAsync() |
Criando o componente Agendamentos.razor
Este componente é opcional e apenas vai exibir uma lista de agendamentos obtidos a partir da API AgendaController:
@page "/agenda" @inject JsonPlaceHolderAgenda _jsonPlaceHolderAgenda; <h1>Agendamentos</h1> @if (agendamentos != null) @code{ protected override async Task OnInitializedAsync() |
Neste código estamos acessando a API usando uma instância da classe JsonPlaceHolderAgenda. Assim precisamos criar esta classe na pasta Services:
using BlazorAppAgenda.Shared; using Microsoft.AspNetCore.Components; using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Json; using System.Threading.Tasks;
namespace BlazorAppAgenda.Client.Services public async Task<List<Agenda>> GetAgendamentos() if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) |
Neste código temos o método GetAgendamentos que retorna a lista da agenda e acessa o endpoint /api/agenda. Note que se o usuário não estiver autorizado estamos navegando para o componente de rota 'notauthorized'.
Vamos criar o componente NotAuthorized.razor na pasta Pages apenas para informar que o usuário não esta autorizado:
@page "/notauthorized" <h1>Usuário Não Autorizado</h1> |
Criando o componente LoginDisplay e atualizando o MainLayout
Como tarefa final
vamos adicionar um componente chamado LoginDisplay
na pasta Shared do projeto
Client e, em seguida, atualizar o componente
MainLayout para usá-lo.
O componente LoginDisplay vai permitir mostrar os
links para Registro e Login se o usuário não
estiver autenticado e exibir o email do usuário logado e o link para
fazer o logout:
<AuthorizeView> <Authorized> Hello, @context.User.Identity.Name! <a href="LogOut">Log out</a> </Authorized> <NotAuthorized> <a href="Register">Register</a> <a href="Login">Log in</a> </NotAuthorized> </AuthorizeView> |
Agora temos que atualizar o arquivo MainLayout usando este componente para exibir os links:
@inherits LayoutComponentBase
<div class="page"> <div class="main"> <div class="content px-4"> |
Agora só falta registrar todos os serviços criados no projeto no arquivo Program do projeto Client:
using BlazorAppAgenda.Client.Services; using Blazored.LocalStorage; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.Extensions.DependencyInjection; using System; using System.Net.Http; using System.Threading.Tasks;
namespace BlazorAppAgenda.Client builder.Services.AddScoped(sp => new HttpClient builder.Services.AddBlazoredLocalStorage(); await builder.Build().RunAsync(); |
Nota: Eu atualizei o componente Index.razor para exibir uma imagem e também atualizei o componente NavMenu.razor para exibir o menu com apenas dois links.
Agora é só alegria...
Vamos executar o projeto e testar ...
Executando o projeto inicialmente teremos o seguinte resultado:
Ao tentar acessar a lista de agendamentos, como não estamos logados iremos receber o resultado abaixo:
Vamos então clicar no link Register e criar um novo usuário:
Fazendo o registro do usuário com sucesso seremos redirecionados para a página de Login:
Fazendo o Login com sucesso teremos a página a seguir:
Note o email do usuário e o link par Logout sendo exibidos.
Acessando o link para exibir os agendamentos teremos o resultado a seguir:
Examinando o LocalStorage da aplicação no navegador Chrome podemos constatar o token JWT sendo armazenado:
Pegue o projeto aqui : BlazorAppAgenda.zip (sem as referências)
"Bem-aventurados
os pobres de espírito, porque deles é o reino dos céus; Bem-aventurados os que
choram, porque eles serão consolados; "
Mateus 5:3,4
Referências:
ASP .NET Core - Implementando a segurança com
ASP.NET Core MVC - Criando um Dashboard .
C# - Gerando QRCode - Macoratti
ASP .NET - Gerando QRCode com a API do Google
ASP .NET Core 2.1 - Como customizar o Identity
Usando o ASP .NET Core Identity - Macoratti
ASP .NET Core - Apresentando o IdentityServer4
ASP .NET Core 3.1 - Usando Identity de cabo a rabo