Exercício – utilizar afirmações com autorização baseada em políticas
Na unidade anterior, aprendeu a diferença entre autenticação e autorização. Também aprendeu como as afirmações são utilizadas pelas políticas de autorização. Nesta unidade, vai utilizar a Identidade para armazenar afirmações e aplicar políticas de acesso condicional.
Proteger a lista de pizzas
Recebeu um novo requisito de que a página Lista de Pizzas só deve estar visível para utilizadores autenticados. Além disso, apenas os administradores podem criar e eliminar pizzas. Vamos bloqueá-lo.
Em 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 utilizador 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 ao
Authorize
adicionar a seguinte linha àsusing
diretivas na parte superior do ficheiro: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 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 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"); }
Vai ocultar os elementos de IU de criação/eliminação para não administradores no próximo passo. Isto não impede que um adversário com uma ferramenta como HttpRepl ou Postman aceda diretamente a estes pontos finais. Adicionar esta verificação garante que, se for tentado, é devolvido um código de estado HTTP 403.
Em Pages/Pizza.cshtml, adicione verificações para ocultar elementos da IU 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> }
Botão Ocultar piza Eliminar
<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 IU que só devem ser acessíveis aos administradores sejam compostos quando o utilizador autenticado é um administrador.
Aplicar uma política de autorização
Há mais uma coisa que devias bloquear. Existe uma página que deve ser acessível apenas aos administradores, convenientemente denominada Pages/AdminsOnly.cshtml. Vamos criar uma política para verificar a IsAdmin=True
afirmação.
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 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 ao aplicar aAdmin
política. Os utilizadores autenticados que não cumprirem os requisitos de política recebem uma mensagem de Acesso negado.Dica
Em alternativa, poderia ter modificado AdminsOnly.cshtml.cs. Nesse caso, adicionaria
[Authorize(Policy = "Admin")]
como um atributo naAdminsOnlyModel
classe. Uma vantagem para aAuthorizePage
abordagem mostrada acima é que a Página do Razor que está a ser protegida não requer modificações. Em vez disso, o aspeto da autorização é gerido 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 a ligação Administração no cabeçalho se o utilizador não for um administrador.
Adicionar a IsAdmin
afirmação a um utilizador
Para determinar que utilizadores devem obter a IsAdmin=True
afirmação, a sua aplicação irá depender de 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 afirmaçã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 contentor IoC. ContémIConfiguration
valores de appsettings.json e é atribuído a uma propriedade só de 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 é lida aConfiguration
partir da propriedade e atribuída aadminEmail
. - O operador
??
de agrupamento nulo é utilizado para garantir queadminEmail
está definido comostring.Empty
se não houver um valor correspondente em appsettings.json. - Se o e-mail do utilizador for confirmado com êxito:
- O endereço do utilizador é comparado a
adminEmail
.string.Compare()
é utilizado para comparação não sensível a maiúsculas e minúsculas. - Na classe
UserManager
, o métodoAddClaimAsync
é invocado para guardar uma afirmaçãoIsAdmin
na tabelaAspNetUserClaims
.
- O endereço do utilizador é comparado a
- A
Adicione o seguinte código na parte superior do ficheiro. Resolve as referências de
Claim
classe noOnGetAsync
método:using System.Security.Claims;
Testar afirmação de administrador
Vamos fazer um último teste para verificar a nova funcionalidade de administrador.
Certifique-se de que guardou todas as suas alterações.
Execute a aplicação com
dotnet run
.Navegue para a 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. Repare que o utilizador não é apresentado elementos de IU para eliminar ou criar pizzas.
Não existe nenhuma ligação administradores no cabeçalho. Na barra de endereço do browser, 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
.Tal como anteriormente, confirme o endereço de e-mail do novo utilizador e inicie sessão.
Depois de iniciar sessão com o novo utilizador administrativo, selecione a ligação Lista de Pizzas no cabeçalho.
O utilizador administrativo pode criar e eliminar pizzas.
Selecione a ligação Administradores no cabeçalho.
É apresentada a página AdminsOnly .
Examinar a tabela AspNetUserClaims
Com 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
É apresentado um separador com resultados semelhantes aos seguintes:
ClaimType | ClaimValue | |
---|---|---|
admin@contosopizza.com | IsAdmin | Verdadeiro |
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, modificou a aplicação para armazenar afirmações e aplicar políticas de acesso condicional.