Esercizio - Usare le attestazioni con l'autorizzazione basata su criteri
Nell'unità precedente si è appresa la differenza tra autenticazione e autorizzazione. Si è anche osservato in che modo le attestazioni vengono usate dai criteri per l'autorizzazione. In questa unità si userà l'identità per archiviare le attestazioni e applicare i criteri per l'accesso condizionale.
Proteggere l'elenco delle pizze
È stato ricevuto un nuovo requisito in base al quale la pagina Elenco delle pizze deve essere visibile solo agli utenti autenticati. Inoltre, solo gli amministratori sono autorizzati a modificare, creare ed eliminare le pizze. Si vedrà ora come bloccarlo.
In Pages/Pizza.cshtml.cs applicare le modifiche seguenti:
Aggiungere un attributo
[Authorize]
alla classePizzaModel
.[Authorize] public class PizzaModel : PageModel
L'attributo descrive i requisiti di autorizzazione utente per la pagina. In questo caso, non sono previsti requisiti oltre all'autenticazione dell'utente. Gli utenti anonimi non sono autorizzati a visualizzare la pagina e vengono reindirizzati alla pagina di accesso.
Risolvere il riferimento a
Authorize
aggiungendo la riga seguente alle direttiveusing
all'inizio del file:using Microsoft.AspNetCore.Authorization;
Aggiungere la proprietà seguente alla classe
PizzaModel
:[Authorize] public class PizzaModel : PageModel { public bool IsAdmin => HttpContext.User.HasClaim("IsAdmin", bool.TrueString); public List<Pizza> pizzas = new();
Il codice precedente determina se l'utente autenticato ha un'attestazione
IsAdmin
con un valoreTrue
. Il codice ottiene informazioni sull'utente autenticato dalHttpContext
nella classe padrePageModel
. Il risultato di questa valutazione è accessibile tramite una proprietà di sola lettura denominataIsAdmin
.Aggiungere
if (!IsAdmin) return Forbid();
all'inizio di entrambi i metodiOnPost
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"); }
Nel passaggio successivo verranno nascosti gli elementi dell'interfaccia utente di creazione/eliminazione per gli utenti non amministratori. Ciò non impedisce a un antagonista con uno strumento come HttpRepl o curl di accedere direttamente a questi endpoint. L'aggiunta di questo controllo garantisce che, in caso di tentativo, venga restituito un codice di stato HTTP 403.
In Pages/Pizza.cshtml aggiungere i controlli per nascondere gli elementi dell'interfaccia utente amministratore agli utenti non amministratori:
Nascondi modulo Nuova 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> }
Nascondi pulsante Elimina 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>
In seguito alle modifiche precedenti, il rendering degli elementi dell'interfaccia utente che devono essere accessibili solo agli amministratori verrà eseguito solo quando l'utente autenticato è un amministratore.
Applicare un criterio di autorizzazione
È opportuno bloccare un altro elemento. Esiste una pagina che deve essere accessibile solo agli amministratori, denominata Pages/AdminsOnly.cshtml. Si procederà quindi alla creazione di un criterio per controllare l'attestazione IsAdmin=True
.
In Program.cs apportare le modifiche seguenti:
Incorporare il codice evidenziato seguente:
// 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();
Il codice precedente definisce un criterio di autorizzazione denominato
Admin
. Il criterio richiede che l'utente sia autenticato e abbia un'attestazioneIsAdmin
impostata suTrue
.Modificare la chiamata a
AddRazorPages
come indicato di seguito:builder.Services.AddRazorPages(options => options.Conventions.AuthorizePage("/AdminsOnly", "Admin"));
La chiamata al metodo
AuthorizePage
protegge la route della pagina Razor /AdminsOnly applicando il criterioAdmin
. Agli utenti autenticati che non soddisfano i requisiti del criterio viene mostrato un messaggio di accesso negato.Suggerimento
In alternativa, è possibile modificare AdminsOnly.cshtml.cs. In tal caso, si aggiungerà
[Authorize(Policy = "Admin")]
come attributo nella classeAdminsOnlyModel
. Un vantaggio dell'approccio diAuthorizePage
illustrato sopra sta nel fatto che la pagina Razor protetta non richiede alcuna modifica. L'aspetto relativo all'autorizzazione è invece gestito in Program.cs.
In Pages/Shared/_Layout.cshtml incorporare le modifiche seguenti:
<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>
La modifica precedente nasconde in modo condizionale il collegamento Admin nell'intestazione se l'utente non è un amministratore. Usa la proprietà
Context
della classeRazorPage
per accedere alHttpContext
contenente le informazioni sull'utente autenticato.
Aggiungere l'attestazione IsAdmin
a un utente
Per determinare quali utenti devono ottenere l'attestazione IsAdmin=True
, l'app si baserà su un indirizzo di posta elettronica confermato per identificare l'amministratore.
In appsettings.json aggiungere la proprietà evidenziata:
{ "AdminEmail" : "admin@contosopizza.com", "Logging": {
Sarà l'indirizzo di posta elettronica confermato a cui viene assegnata l'attestazione.
In Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs apportare le modifiche seguenti:
Incorporare il codice evidenziato seguente:
public class ConfirmEmailModel : PageModel { private readonly UserManager<RazorPagesPizzaUser> _userManager; private readonly IConfiguration Configuration; public ConfirmEmailModel(UserManager<RazorPagesPizzaUser> userManager, IConfiguration configuration) { _userManager = userManager; Configuration = configuration; }
La modifica precedente modifica il costruttore per ricevere un oggetto
IConfiguration
dal contenitore IoC. L'oggettoIConfiguration
contiene i valori di appsettings.json e viene assegnato a una proprietà di sola lettura denominataConfiguration
.Applicare le modifiche evidenziate al metodo
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(); }
Nel codice precedente:
- La stringa
AdminEmail
viene letta dalla proprietàConfiguration
e assegnata aadminEmail
. - L'operatore di coalescenza di valori Null
??
viene usato per assicurarsiadminEmail
che sia impostato sustring.Empty
se non è presente alcun valore corrispondente in appsettings.json. - Se l'indirizzo di posta elettronica dell'utente viene confermato:
- L'indirizzo dell'utente viene confrontato con
adminEmail
. Si usastring.Compare()
per il confronto senza distinzione tra maiuscole e minuscole. - Viene richiamato il metodo
AddClaimAsync
della classeUserManager
per salvare un'attestazioneIsAdmin
nella tabellaAspNetUserClaims
.
- L'indirizzo dell'utente viene confrontato con
- La stringa
Aggiungere il codice seguente all'inizio del file. Risolve i riferimenti alla classe
Claim
nel metodoOnGetAsync
:using System.Security.Claims;
Testare le attestazioni amministratore
Eseguire un ultimo test per verificare la nuova funzionalità di amministratore.
Assicurarsi di avere salvato tutte le modifiche.
Eseguire l'app con
dotnet run
.Passare all'app e accedere con un utente esistente, se non è già stato eseguito l'accesso. Selezionare Pizza List (Elenco delle pizze) nell'intestazione. Si noti che all'utente non vengono mostrati elementi dell'interfaccia utente per eliminare o creare pizze.
Nell'intestazione non è presente alcun collegamento Admins. Nella barra degli indirizzi del browser passare direttamente alla pagina AdminsOnly. Sostituire
/Pizza
nell'URL con/AdminsOnly
.L'utente non è autorizzato a passare alla pagina. Viene visualizzato un messaggio di accesso negato.
Selezionare Logout (Disconnetti).
Registrare un nuovo utente con l'indirizzo
admin@contosopizza.com
.Come in precedenza, confermare l'indirizzo di posta elettronica del nuovo utente ed eseguire l'accesso.
Una volta eseguito l'accesso con il nuovo utente amministratore, fare clic sul collegamento Pizza List (Elenco delle pizze) nell'intestazione.
L'utente amministratore può creare ed eliminare pizze.
Fare clic sul collegamento Admins nell'intestazione.
Verrà visualizzata la pagina AdminsOnly.
Esaminare la tabella AspNetUserClaims
Usando l'estensione SQL Server in VS Code, eseguire la query seguente:
SELECT u.Email, c.ClaimType, c.ClaimValue
FROM dbo.AspNetUserClaims AS c
INNER JOIN dbo.AspNetUsers AS u
ON c.UserId = u.Id
Verrà visualizzata una scheda con risultati simili ai seguenti:
ClaimType | ClaimValue | |
---|---|---|
admin@contosopizza.com | IsAdmin | Vero |
L'attestazione IsAdmin
viene archiviata come coppia chiave-valore nella tabella AspNetUserClaims
. Il record AspNetUserClaims
è associato al record utente nella tabella AspNetUsers
.
Riepilogo
In questa unità è stata modificata l'app per archiviare le attestazioni e applicare i criteri per l'accesso condizionale.