Übung: Verwenden von Ansprüchen mit richtlinienbasierter Autorisierung

Abgeschlossen

In vorherigen Lerneinheit haben Sie den Unterschied zwischen Authentifizierung und Autorisierung kennengelernt. Sie haben auch gelernt, wie Ansprüche von Richtlinien zur Autorisierung verwendet werden. In dieser Lerneinheit verwenden Sie Identity, um Ansprüche zu speichern und Richtlinien für bedingten Zugriff anzuwenden.

Sichern der Pizzaliste

Sie haben eine neue Anforderung erhalten, dass die Seite mit der Pizzaliste nur für authentifizierte Benutzer sichtbar sein soll. Darüber hinaus sind nur Administratoren berechtigt, Pizzen zu bearbeiten, zu erstellen und zu löschen. Lassen Sie uns diese Funktionen sperren.

  1. Nehmen Sie in Pages/Pizza.cshtml.cs die folgenden Änderungen vor:

    1. Fügen Sie ein [Authorize]-Attribut zur PizzaModel-Klasse hinzu.

      [Authorize]
      public class PizzaModel : PageModel
      

      Das Attribut beschreibt die Benutzerautorisierungsanforderungen für die Seite. In diesem Fall gibt es keine Anforderungen, die über den authentifizierten Benutzer hinausgehen. Anonyme Benutzer können die Seite nicht anzeigen und werden zur Anmeldeseite weitergeleitet.

    2. Lösen Sie den Verweis auf Authorize auf, indem Sie am Anfang der Datei die folgende Zeile zu den using-Direktiven hinzufügen:

      using Microsoft.AspNetCore.Authorization;
      
    3. Fügen Sie der PizzaModel-Klasse die folgende Eigenschaft hinzu:

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

      Mit dem obigen Code wird ermittelt, ob der authentifizierte Benutzer über einen IsAdmin-Anspruch mit dem Wert True verfügt. Auf das Ergebnis dieser Auswertung wird mithilfe einer schreibgeschützten Eigenschaft namens IsAdmin zugegriffen.

    4. Fügen Sie if (!IsAdmin) return Forbid(); am Anfang der beiden Methoden OnPost und OnPostDelete hinzu:

      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");
      }
      

      Sie werden die Elemente der Benutzeroberfläche zum Erstellen/Löschen für Nicht-Administratoren im nächsten Schritt ausblenden. Dadurch wird nicht verhindert, dass ein Angreifer mit einem Tool wie HttpRepl oder Postman direkt auf diese Endpunkte zugreift. Durch das Hinzufügen dieser Überprüfung wird sichergestellt, dass bei einem entsprechenden Versuch ein HTTP 403-Statuscode zurückgegeben wird.

  2. Fügen Sie in Pages/Pizza.cshtml Prüfungen hinzu, um Elemente der Benutzeroberfläche, die für Administratoren vorgesehen sind, für Nicht-Administratoren auszublenden:

    Ausblenden des Formulars Neue 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>
    }
    

    Ausblenden der Schaltfläche Pizza löschen

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

    Die obigen Änderungen führen dazu, dass Elemente der Benutzeroberfläche, auf die nur Administratoren zugreifen können sollen, nur dann gerendert werden, wenn der authentifizierte Benutzer ein Administrator ist.

Anwenden einer Autorisierungsrichtlinie

Es gibt noch etwas, was Sie sperren sollten. Es gibt eine Seite Pages/AdminsOnly.cshtml, auf die nur Administratoren zugreifen können sollten. Lassen Sie uns eine Richtlinie erstellen, um den IsAdmin=True-Anspruch zu überprüfen.

  1. Nehmen Sie in Program.cs die folgenden Änderungen vor:

    1. Fügen Sie den folgenden hervorgehobenen Code ein:

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

      Im vorangehenden Code wird eine Autorisierungsrichtlinie namens Admin definiert. Die Richtlinie erfordert, dass der Benutzer authentifiziert ist und über einen auf True festgelegten IsAdmin-Anspruch verfügt.

    2. Ändern Sie den Aufruf von AddRazorPages wie folgt:

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

      Der Aufruf der AuthorizePage-Methode schützt die Razor Pages-Route /AdminsOnly, indem die Admin-Richtlinie angewendet wird. Authentifizierte Benutzer, die die Anforderungen der Richtlinie nicht erfüllen, erhalten die Nachricht Access denied (Zugriff verweigert).

      Tipp

      Alternativ könnten Sie stattdessen AdminsOnly.cshtml.cs ändern. In diesem Fall würden Sie [Authorize(Policy = "Admin")] als Attribut für die AdminsOnlyModel-Klasse hinzufügen. Ein Vorteil des oben gezeigten AuthorizePage-Ansatzes besteht darin, dass für die geschützte Razor-Seite keine Anpassungen erforderlich sind. Die Autorisierung wird stattdessen in Program.cs verwaltet.

  2. Implementieren Sie in Pages/Shared/_Layout.cshtml die folgenden Änderungen:

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

    Die obige Änderung blendet den Admin-Link in der Kopfzeile aus, wenn der Benutzer kein Administrator ist.

Hinzufügen des IsAdmin-Anspruchs für einen Benutzer

Um zu bestimmen, welche Benutzer den IsAdmin=True-Anspruch erhalten sollen, verwendet die App eine bestätigte E-Mail-Adresse, um den Administrator zu identifizieren.

  1. Fügen Sie in appsettings.json die hervorgehobene Eigenschaft hinzu:

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

    Dies ist die bestätigte E-Mail-Adresse, der der Anspruch zugewiesen wird.

  2. Nehmen Sie in Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs die folgenden Änderungen vor:

    1. Fügen Sie den folgenden hervorgehobenen Code ein:

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

      Die obige Änderung ändert den Konstruktor für den Empfang einer IConfiguration vom IoC-Container. IConfiguration enthält Werte aus appsettings.json und wird einer schreibgeschützten Eigenschaft namens Configuration zugewiesen.

    2. Wenden Sie die hervorgehobenen Änderungen zur OnGetAsync-Methode hinzu:

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

      Im obigen Code:

      • Die AdminEmail-Zeichenfolge wird aus der Configuration-Eigenschaft gelesen und adminEmail zugewiesen.
      • Der NULL-Sammeloperator ?? wird verwendet, um sicherzustellen, dass adminEmail auf string.Empty festgelegt wird, wenn kein entsprechender Wert in appsettings.json vorhanden ist.
      • Wenn die E-Mail des Benutzers erfolgreich bestätigt wird:
        • Die Adresse des Benutzers wird verglichen mit adminEmail. string.Compare() wird für den Vergleich ohne Berücksichtigung der Groß- und Kleinschreibung verwendet.
        • Wird die AddClaimAsync-Methode der UserManager-Klasse aufgerufen, um einen IsAdmin-Anspruch in der AspNetUserClaims-Tabelle zu speichern.
    3. Fügen Sie den folgenden Code ganz oben in der Datei ein. Damit werden die Verweise auf die Claim-Klasse in der OnGetAsync-Methode aufgelöst:

      using System.Security.Claims;
      

Testen des Administratoranspruchs

Lassen Sie uns einen letzten Test ausführen, um die neue Administratorfunktionalität zu überprüfen.

  1. Vergewissern Sie sich, dass Sie alle Änderungen gespeichert haben.

  2. Führen Sie die App mit dotnet run aus.

  3. Navigieren Sie zu Ihrer App, und melden Sie sich bei einem vorhandenen Benutzer an, wenn Sie noch nicht angemeldet sind. Wählen Sie Pizza List in der Kopfzeile aus. Beachten Sie, dass für den Benutzer keine Benutzeroberflächenelemente zum Löschen oder Erstellen von Pizzen angezeigt werden.

  4. Es gibt keinen Admins-Link in der Kopfzeile. Navigieren Sie in der Adressleiste des Browsers direkt zur Seite AdminsOnly. Ersetzen Sie /Pizza in der URL durch /AdminsOnly.

    Der Benutzer kann die Seite nicht aufrufen. Die Meldung Access denied (Zugriff verweigert) wird angezeigt.

  5. Klicken Sie auf Abmelden.

  6. Registrieren Sie einen neuen Benutzer mit der Adresse admin@contosopizza.com.

  7. Bestätigen Sie wie zuvor die E-Mail-Adresse des neuen Benutzers, und melden Sie sich an.

  8. Klicken Sie auf den Link Pizza List in der Kopfzeile, sobald Sie sich als neuer Administrator angemeldet haben.

    Der Administrator kann Pizzas erstellen und löschen.

  9. Wählen Sie den Admins-Link in der Kopfzeile aus.

    Die AdminsOnly-Seite wird angezeigt.

Untersuchen der AspNetUserClaims-Tabelle

Führen Sie unter Verwendung der SQL Server-Erweiterung in VS Code die folgende Abfrage aus:

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

Eine Registerkarte mit ähnlichen Ergebnissen wie folgt wird angezeigt:

E-Mail ClaimType ClaimValue
admin@contosopizza.com IsAdmin True

Der IsAdmin-Anspruch wird als Schlüssel-Wert-Paar in der AspNetUserClaims-Tabelle gespeichert. Der Eintrag AspNetUserClaims wird dem Benutzereintrag in der AspNetUsers-Tabelle zugeordnet.

Zusammenfassung

In dieser Lerneinheit haben Sie die App geändert, um Ansprüche zu speichern und Richtlinien für bedingten Zugriff anzuwenden.