Exercício – utilizar afirmaçõ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 pelas políticas de autorização. Nesta unidade, você usa Identity para armazenar declarações e aplicar políticas de acesso condicional.
Proteja 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 os administradores têm permissão para criar e excluir pizzas. Vamos bloqueá-lo.
No Pages/Pizza.cshtml.cs, aplique as seguintes alterações:
Adicione um
[Authorize]
atributo àPizzaModel
classe.[Authorize] public class PizzaModel : PageModel
O atributo descreve os requisitos de autorização do usuário para a página. Nesse caso, não há requisitos além de o utilizador ser autenticado. Os utilizadores anónimos não têm permissão para ver a página e são redirecionados para a página de início de sessão.
Resolva a referência adicionando
Authorize
a seguinte linha àsusing
diretivas 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 utilizador autenticado tem uma afirmação
IsAdmin
com um valor deTrue
. O código obtém informações sobre o usuário autenticado daHttpContext
classe paiPageModel
. O resultado desta avaliação é acedido através de uma propriedade só de leitura com o nomeIsAdmin
.Adicione
if (!IsAdmin) return Forbid();
ao início dosOnPost
métodos e :OnPostDelete
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ê 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 acesse esses endpoints 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 novo formulário de 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 da interface do usuário que devem ser acessíveis apenas aos administradores sejam renderizados somente quando o usuário autenticado for 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 apenas para administradores, convenientemente chamada Pages/AdminsOnly.cshtml. Vamos criar uma política para verificar a IsAdmin=True
reivindicação.
No 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 com o nome
Admin
. A política requer que o utilizador seja autenticado e tenha uma afirmaçãoIsAdmin
definida comoTrue
.Modifique a chamada da
AddRazorPages
seguinte forma:builder.Services.AddRazorPages(options => options.Conventions.AuthorizePage("/AdminsOnly", "Admin"));
A
AuthorizePage
chamada de método protege a rota /AdminsOnly Razor Page aplicando aAdmin
política. Os utilizadores autenticados que não cumprirem os requisitos de política recebem uma mensagem de Acesso negado.Gorjeta
Alternativamente, você poderia ter modificado AdminsOnly.cshtml.cs. Nesse caso, você adicionaria
[Authorize(Policy = "Admin")]
como um atributo naAdminsOnlyModel
classe. Uma vantagem da abordagem mostradaAuthorizePage
acima é que a Razor Page que está sendo protegida não requer modificações. Em vez disso, o aspeto 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 Admin no cabeçalho se o usuário não for um administrador. Ele usa a
Context
propriedade daRazorPage
classe para acessar oHttpContext
que contém as informações sobre o usuário autenticado.
Adicionar a IsAdmin
declaração a um utilizador
Para determinar quais usuários devem receber a IsAdmin=True
declaração, seu aplicativo vai contar com um endereço de e-mail confirmado para identificar o administrador.
Em appsettings.json, adicione a propriedade realçada:
{ "AdminEmail" : "admin@contosopizza.com", "Logging": {
Este é o endereço de e-mail confirmado que recebe a reivindicação atribuída.
Em Áreas/Identidade/Páginas/Conta/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
.Incorpore as alterações realçadas no 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
AdminEmail
cadeia de caracteres é lida daConfiguration
propriedade e atribuída aadminEmail
. - O operador
??
de coalescência nula é usado para garantiradminEmail
que seja definido comostring.Empty
se não houver nenhum valor correspondente em appsettings.json. - Se o e-mail do usuário for confirmado com sucesso:
- O endereço do usuário é comparado ao
adminEmail
.string.Compare()
é usado para comparação que não diferencia maiúsculas de minúsculas. - Na classe
UserManager
, o métodoAddClaimAsync
é invocado para guardar uma afirmaçãoIsAdmin
na tabelaAspNetUserClaims
.
- O endereço do usuário é comparado ao
- A
Adicione o seguinte código na parte superior do ficheiro. Ele resolve as
Claim
referências de classe noOnGetAsync
método:using System.Security.Claims;
Testar afirmação de administrador
Vamos fazer um último teste para verificar a nova funcionalidade do administrador.
Certifique-se de que guardou todas as alterações.
Execute o aplicativo com
dotnet run
o .Navegue até à sua aplicação e inicie sessão com um utilizador existente, se ainda não tiver sessão iniciada. Selecione Lista de pizzas no cabeçalho. Observe que não são apresentados elementos da interface do usuário ao usuário para excluir ou criar pizzas.
Não há nenhum link Administradores no cabeçalho. Na barra de endereço do navegador, navegue diretamente para a página AdminsOnly . Substitua
/Pizza
no URL por/AdminsOnly
.O utilizador é proibido de navegar para a página. É apresentada uma mensagem de Acesso negado.
Selecione Logout (Terminar sessão).
Registe um novo utilizador com o endereço
admin@contosopizza.com
.Como antes, confirme o endereço de e-mail do novo usuário e faça login.
Depois de entrar com o novo usuário administrativo, selecione o link Lista de pizzas no cabeçalho.
O usuário administrativo pode criar e excluir pizzas.
Selecione o link Administradores no cabeçalho.
A página AdminsOnly é exibida.
Examinar a tabela AspNetUserClaims
Usando a extensão do 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
É apresentado um separador com resultados semelhantes aos seguintes:
Tipo de reivindicação | ClaimValue | |
---|---|---|
admin@contosopizza.com | IsAdmin | True |
A afirmação IsAdmin
é armazenada como par chave-valor na tabela AspNetUserClaims
. O registo AspNetUserClaims
é associado ao registo de utilizador na tabela AspNetUsers
.
Resumo
Nesta unidade, você modificou o aplicativo para armazenar declarações e aplicar políticas de acesso condicional.