Övning – Använd anspråk med principbaserad auktorisering

Slutförd

I föregående lektion lärde du dig skillnaden mellan autentisering och auktorisering. Du har också lärt dig hur anspråk används av principer för auktorisering. I den här lektionen använder du Identity för att lagra anspråk och tillämpa principer för villkorlig åtkomst.

Säkra pizzalistan

Du har fått ett nytt krav på att sidan Pizzalista endast ska vara synlig för autentiserade användare. Dessutom får endast administratörer skapa och ta bort pizzor. Låt oss låsa den.

  1. I Pages/Pizza.cshtml.cs tillämpar du följande ändringar:

    1. Lägg till ett [Authorize] attribut i PizzaModel klassen .

      [Authorize]
      public class PizzaModel : PageModel
      

      Attributet beskriver användarens auktoriseringskrav för sidan. I det här fallet finns det inga krav utöver användaren som autentiseras. Anonyma användare får inte visa sidan och omdirigeras till inloggningssidan.

    2. Lös referensen till genom att lägga till Authorize följande rad i direktiven using överst i filen:

      using Microsoft.AspNetCore.Authorization;
      
    3. Lägg till följande egenskap i klassen PizzaModel:

      [Authorize]
      public class PizzaModel : PageModel
      {
          public bool IsAdmin => HttpContext.User.HasClaim("IsAdmin", bool.TrueString);
      
          public List<Pizza> pizzas = new();
      

      Koden ovan fastställer om den autentiserade användaren har ett IsAdmin-anspråk med värdet True. Resultatet av den här utvärderingen nås via en skrivskyddad egenskap med namnet IsAdmin.

    4. Lägg till if (!IsAdmin) return Forbid(); i början av metodernaOnPost och 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");
      }
      

      Du kommer att dölja elementen för att skapa/ta bort användargränssnitt för icke-administratörer i nästa steg. Det hindrar inte en angripare med ett verktyg som HttpRepl eller Postman från att komma åt dessa slutpunkter direkt. Om du lägger till den här kontrollen ser du till att om detta görs returneras en HTTP 403-statuskod.

  2. I Pages/Pizza.cshtml lägger du till kontroller för att dölja administratörsgränssnittselement från icke-administratörer:

    Dölj formulär för ny 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>
    }
    

    Dölj knappen Ta bort 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>
    

    Föregående ändringar gör att gränssnittselement som endast ska vara tillgängliga för administratörer återges när den autentiserade användaren är administratör.

Tillämpa en auktoriseringsprincip

Det är en sak till du borde låsa. Det finns en sida som endast ska vara tillgänglig för administratörer med namnet Pages/AdminsOnly.cshtml. Nu ska vi skapa en princip för att kontrollera anspråket IsAdmin=True .

  1. Gör följande ändringar i Program.cs:

    1. Inkludera följande markerade kod:

      // 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();
      

      Koden ovan definierar en auktoriseringsprincip med namnet Admin. Principen kräver att användaren autentiseras och har ett IsAdmin-anspråk inställt på True.

    2. Ändra anropet till AddRazorPages enligt följande:

      builder.Services.AddRazorPages(options =>
          options.Conventions.AuthorizePage("/AdminsOnly", "Admin"));
      

      Metodanropet AuthorizePage skyddar väg för /AdminsOnly Razor-sidan genom att Admin tillämpa principen. För autentiserade användare som inte uppfyller principkraven visas ett meddelande om nekad åtkomst.

      Tips

      Du kan också ha ändrat AdminsOnly.cshtml.cs i stället. I så fall skulle du lägga till [Authorize(Policy = "Admin")] som ett attribut i AdminsOnlyModel klassen . En fördel med metoden AuthorizePage ovan är att Razor-sidan som skyddas inte kräver några ändringar. Auktoriseringsaspekten hanteras i stället i Program.cs.

  2. I Pages/Shared/_Layout.cshtml lägger du till följande ändringar:

    <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>
    

    Föregående ändring döljer villkorsstyrt länken Admin i rubriken om användaren inte är administratör.

Lägga till anspråket IsAdmin till en användare

För att avgöra vilka användare som ska få anspråket IsAdmin=True kommer din app att förlita sig på en bekräftad e-postadress för att identifiera administratören.

  1. Lägg till den markerade egenskapen i appsettings.json:

    {
      "AdminEmail" : "admin@contosopizza.com",
      "Logging": {
    

    Det här är den bekräftade e-postadressen som tilldelas anspråket.

  2. Gör följande ändringar i Områden/Identitet/Sidor/Konto/ConfirmEmail.cshtml.cs:

    1. Inkludera följande markerade kod:

      public class ConfirmEmailModel : PageModel
      {
          private readonly UserManager<RazorPagesPizzaUser> _userManager;
          private readonly IConfiguration Configuration;
      
          public ConfirmEmailModel(UserManager<RazorPagesPizzaUser> userManager,
                                      IConfiguration configuration)
          {
              _userManager = userManager;
              Configuration = configuration;
          }
      
      

      Föregående ändring ändrar konstruktorn för att ta emot en IConfiguration från IoC-containern. IConfiguration innehåller värden från appsettings.json och tilldelas till en skrivskyddad egenskap med namnet Configuration.

    2. Tillämpa de markerade ändringarna i metoden 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();
      }
      

      I koden ovan sker följande:

      • Strängen AdminEmail läss från Configuration egenskapen och tilldelas till adminEmail.
      • Operatorn ?? null-coalescing används för att säkerställa adminEmail att anges till string.Empty om det inte finns något motsvarande värde i appsettings.json.
      • Om användarens e-post har bekräftats:
        • Användarens adress jämförs med adminEmail. string.Compare() används för skiftlägesokänslig jämförelse.
        • UserManager-klassens AddClaimAsync-metod anropas för att spara ett IsAdmin-anspråk i tabellen AspNetUserClaims.
    3. Lägg till följande kod högst upp i filen. Den löser Claim klassreferenserna i OnGetAsync metoden :

      using System.Security.Claims;
      

Testa administratörsanspråk

Nu ska vi göra ett sista test för att verifiera de nya administratörsfunktionerna.

  1. Kontrollera att du har sparat alla ändringar.

  2. Kör appen med dotnet run.

  3. Gå till din app och logga in med en befintlig användare, om du inte redan är inloggad. Välj Pizzalista i rubriken. Observera att användaren inte visas gränssnittselement för att ta bort eller skapa pizzor.

  4. Det finns ingen administratörslänk i rubriken. I webbläsarens adressfält navigerar du direkt till sidan AdminsOnly . Ersätt /Pizza i URL:en med /AdminsOnly.

    Användaren tillåts inte att navigera till sidan. Ett meddelande om nekad åtkomst visas.

  5. Välj Logout (Logga ut).

  6. Registrera en ny användare med adressen admin@contosopizza.com.

  7. Som tidigare bekräftar du den nya användarens e-postadress och loggar in.

  8. När du har loggat in med den nya administrativa användaren väljer du länken Pizzalista i rubriken.

    Den administrativa användaren kan skapa och ta bort pizzor.

  9. Välj länken Administratörer i rubriken.

    Sidan AdminsOnly visas.

Granska tabellen AspNetUserClaims

Kör följande fråga med hjälp av SQL Server-tillägget i VS Code:

SELECT u.Email, c.ClaimType, c.ClaimValue
FROM dbo.AspNetUserClaims AS c
    INNER JOIN dbo.AspNetUsers AS u
    ON c.UserId = u.Id

En flik med resultat som liknar följande visas:

E-post ClaimType ClaimValue
admin@contosopizza.com IsAdmin Sant

Anspråket IsAdmin lagras som ett nyckel/värde-par i tabellen AspNetUserClaims. Posten AspNetUserClaims associeras med användarposten i tabellen AspNetUsers.

Sammanfattning

I den här lektionen har du ändrat appen för att lagra anspråk och tillämpa principer för villkorlig åtkomst.