Exercício – Usar declarações com autorização baseada em políticas
Na unidade anterior, você aprendeu a diferença entre autenticação e autorização. Você também aprendeu como as declarações são usadas por políticas para autorização. Nesta unidade, você vai usar o Identity para armazenar declarações e aplicar políticas para acesso condicional.
Proteger a lista de pizzas
Você recebeu um novo requisito de que a página Lista de Pizzas deve estar visível apenas para usuários autenticados. Além disso, apenas administradores têm permissão para editar, criar e excluir produtos. Vamos bloqueá-la.
Em Pages/Pizza.cshtml.cs, aplique as seguintes alterações:
Adicione um atributo
[Authorize]
à classePizzaModel
.[Authorize] public class PizzaModel : PageModel
O atributo descreve os requisitos de autorização de usuário da página. Nesse caso, não há requisitos além do usuário que está sendo autenticado. Os usuários anônimos não têm permissão para ver a página e são redirecionados à página de entrada.
Resolva a referência a
Authorize
adicionando a seguinte linha às diretivasusing
na parte superior do arquivo:using Microsoft.AspNetCore.Authorization;
Adicione a seguinte propriedade à classe
PizzaModel
:[Authorize] public class PizzaModel : PageModel { public bool IsAdmin => HttpContext.User.HasClaim("IsAdmin", bool.TrueString); public List<Pizza> pizzas = new();
O código anterior determina se o usuário autenticado tem uma declaração
IsAdmin
com um valor igual aTrue
. O resultado dessa avaliação é acessado por meio de uma propriedade somente leitura chamadaIsAdmin
.Adicione
if (!IsAdmin) return Forbid();
ao início de ambos os métodosOnPost
eOnPostDelete
:public IActionResult OnPost() { if (!IsAdmin) return Forbid(); if (!ModelState.IsValid) { return Page(); } PizzaService.Add(NewPizza); return RedirectToAction("Get"); } public IActionResult OnPostDelete(int id) { if (!IsAdmin) return Forbid(); PizzaService.Delete(id); return RedirectToAction("Get"); }
Você vai ocultar os elementos da interface do usuário de criação/exclusão para não administradores na próxima etapa. Isso não impede que um adversário com uma ferramenta como HttpRepl ou curl acessem esses pontos de extremidade diretamente. Adicionar essa verificação garante que, se isso for tentado, um código de status HTTP 403 será retornado.
Em Pages/Pizza.cshtml, adicione verificações para ocultar elementos da interface do usuário do administrador de não administradores:
Ocultar formulário Nova pizza
<h1>Pizza List 🍕</h1> @if (Model.IsAdmin) { <form method="post" class="card p-3"> <div class="row"> <div asp-validation-summary="All"></div> </div> <div class="form-group mb-0 align-middle"> <label asp-for="NewPizza.Name">Name</label> <input type="text" asp-for="NewPizza.Name" class="mr-5"> <label asp-for="NewPizza.Size">Size</label> <select asp-for="NewPizza.Size" asp-items="Html.GetEnumSelectList<PizzaSize>()" class="mr-5"></select> <label asp-for="NewPizza.Price"></label> <input asp-for="NewPizza.Price" class="mr-5" /> <label asp-for="NewPizza.IsGlutenFree">Gluten Free</label> <input type="checkbox" asp-for="NewPizza.IsGlutenFree" class="mr-5"> <button class="btn btn-primary">Add</button> </div> </form> }
Ocultar botão Excluir pizza
<table class="table mt-5"> <thead> <tr> <th scope="col">Name</th> <th scope="col">Price</th> <th scope="col">Size</th> <th scope="col">Gluten Free</th> @if (Model.IsAdmin) { <th scope="col">Delete</th> } </tr> </thead> @foreach (var pizza in Model.pizzas) { <tr> <td>@pizza.Name</td> <td>@($"{pizza.Price:C}")</td> <td>@pizza.Size</td> <td>@Model.GlutenFreeText(pizza)</td> @if (Model.IsAdmin) { <td> <form method="post" asp-page-handler="Delete" asp-route-id="@pizza.Id"> <button class="btn btn-danger">Delete</button> </form> </td> } </tr> } </table>
As alterações anteriores fazem com que os elementos de interface do usuário que devem ser acessíveis somente aos administradores sejam renderizados apenas quando o usuário autenticado é um administrador.
Aplicar uma política de autorização
Há mais uma coisa que você deve bloquear. Há uma página que deve ser acessível somente para administradores, convenientemente denominada Pages/AdminsOnly.cshtml. Vamos criar uma política para verificar a declaração IsAdmin=True
.
Em Program.cs, faça as seguintes alterações:
Incorpore o seguinte código realçado:
// Add services to the container. builder.Services.AddRazorPages(); builder.Services.AddTransient<IEmailSender, EmailSender>(); builder.Services.AddSingleton(new QRCodeService(new QRCodeGenerator())); builder.Services.AddAuthorization(options => options.AddPolicy("Admin", policy => policy.RequireAuthenticatedUser() .RequireClaim("IsAdmin", bool.TrueString))); var app = builder.Build();
O código anterior define uma política de autorização chamada
Admin
. A política exige que o usuário seja autenticado e tenha uma declaraçãoIsAdmin
definida comoTrue
.Modifique a chamada para
AddRazorPages
da seguinte maneira:builder.Services.AddRazorPages(options => options.Conventions.AuthorizePage("/AdminsOnly", "Admin"));
A chamada de método
AuthorizePage
protege a rota da Página Razor /AdminsOnly aplicando a políticaAdmin
. Os usuários autenticados que não atenderem aos requisitos da política verão uma mensagem Acesso negado.Dica
Como alternativa, você poderia ter modificado AdminsOnly.cshtml.cs. Nesse caso, você adicionaria
[Authorize(Policy = "Admin")]
como um atributo na classeAdminsOnlyModel
. Uma vantagem para a abordagemAuthorizePage
mostrada acima é que a Página Razor que está sendo protegida não exige modificações. Em vez disso, o aspecto de autorização é gerenciado em Program.cs.
Em Pages/Shared/_Layout.cshtml, incorpore as seguintes alterações:
<ul class="navbar-nav flex-grow-1"> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a> </li> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-page="/Pizza">Pizza List</a> </li> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a> </li> @if (Context.User.HasClaim("IsAdmin", bool.TrueString)) { <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-page="/AdminsOnly">Admins</a> </li> } </ul>
A alteração anterior oculta condicionalmente o link Administrador no cabeçalho se o usuário não é um administrador.
Adicionar a declaração IsAdmin
a um usuário
Para determinar quais usuários devem receber a declaração IsAdmin=True
, seu aplicativo dependerá de um endereço de email confirmado para identificar o administrador.
Em appsettings.json, adicione a propriedade realçada:
{ "AdminEmail" : "admin@contosopizza.com", "Logging": {
Esse é o endereço de email confirmado que recebe a declaração atribuída.
Em Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs, faça as seguintes alterações:
Incorpore o seguinte código realçado:
public class ConfirmEmailModel : PageModel { private readonly UserManager<RazorPagesPizzaUser> _userManager; private readonly IConfiguration Configuration; public ConfirmEmailModel(UserManager<RazorPagesPizzaUser> userManager, IConfiguration configuration) { _userManager = userManager; Configuration = configuration; }
A alteração anterior modifica o construtor para receber um
IConfiguration
do contêiner IoC. OIConfiguration
contém valores de appsettings.json e é atribuído a uma propriedade somente leitura chamadaConfiguration
.Aplique as alterações realçadas ao método
OnGetAsync
:public async Task<IActionResult> OnGetAsync(string userId, string code) { if (userId == null || code == null) { return RedirectToPage("/Index"); } var user = await _userManager.FindByIdAsync(userId); if (user == null) { return NotFound($"Unable to load user with ID '{userId}'."); } code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code)); var result = await _userManager.ConfirmEmailAsync(user, code); StatusMessage = result.Succeeded ? "Thank you for confirming your email." : "Error confirming your email."; var adminEmail = Configuration["AdminEmail"] ?? string.Empty; if(result.Succeeded) { var isAdmin = string.Compare(user.Email, adminEmail, true) == 0 ? true : false; await _userManager.AddClaimAsync(user, new Claim("IsAdmin", isAdmin.ToString())); } return Page(); }
No código anterior:
- A cadeia de caracteres
AdminEmail
é lida da propriedadeConfiguration
e atribuída aadminEmail
. - O operador de avaliação de nulo
??
é usado para garantir queadminEmail
seja definido comostring.Empty
se não houver nenhum valor correspondente em appsettings.json. - Se o email do usuário for confirmado com êxito:
- O endereço do usuário é comparado a
adminEmail
.string.Compare()
é usado para comparação sem diferenciação de maiúsculas de minúsculas. - O método
AddClaimAsync
da classeUserManager
é invocado para salvar uma declaraçãoIsAdmin
na tabelaAspNetUserClaims
.
- O endereço do usuário é comparado a
- A cadeia de caracteres
Adicione o código a seguir no início do arquivo. Ele resolve as referências de classe
Claim
no métodoOnGetAsync
:using System.Security.Claims;
Testar a declaração de administrador
Vamos fazer um último teste para verificar a nova funcionalidade de administrador.
Verifique se você salvou todas as alterações.
Execute o aplicativo com
dotnet run
.Navegue até o aplicativo e entre com um usuário existente, se você ainda não estiver conectado. Selecione Lista de Pizzas no cabeçalho. Observe que o usuário não vê elementos da interface do usuário para excluir ou criar pizzas.
Não há nenhum link de Administradores no cabeçalho. Na barra de endereços do navegador, navegue diretamente até a página AdminsOnly. Substitua
/Pizza
na URL por/AdminsOnly
.É proibido que o usuário navegue até a página. Uma mensagem Acesso negado é exibida.
Selecione Logoff.
Registre um novo usuário com o endereço
admin@contosopizza.com
.Como antes, confirme o endereço de email do novo usuário e entre.
Depois de entrar com o novo usuário administrativo, clique no link Lista de Pizzas no cabeçalho.
O usuário administrativo pode criar e excluir pizzas.
Clique no link Administradores no cabeçalho.
A página AdminsOnly aparece.
Examinar a tabela AspNetUserClaims
Usando a extensão SQL Server no VS Code, execute a seguinte consulta:
SELECT u.Email, c.ClaimType, c.ClaimValue
FROM dbo.AspNetUserClaims AS c
INNER JOIN dbo.AspNetUsers AS u
ON c.UserId = u.Id
Uma guia com resultados semelhantes aos seguintes aparece:
ClaimType | ClaimValue | |
---|---|---|
admin@contosopizza.com | IsAdmin | True |
A declaração IsAdmin
é armazenada como um par chave-valor na tabela AspNetUserClaims
. O registro AspNetUserClaims
é associado ao registro de usuário na tabela AspNetUsers
.
Resumo
Nesta unidade, você modificou o aplicativo para armazenar declarações e aplicar políticas para acesso condicional.