Упражнение. Использование утверждений с авторизацией на основе политик

Завершено

В предыдущем уроке вы узнали о различии между проверкой подлинности и авторизацией. Вы также узнали, как утверждения используются политиками для авторизации. В этом уроке вы используете identity для хранения утверждений и применения политик условного доступа.

Защита страницы "Список пицц"

Вы получили новое требование о том, что страница "Список пицц" должна отображаться только для пользователей, прошедших проверку подлинности. Кроме того, возможность создавать и удалять пиццы должна быть только у администраторов. Давайте реализуем соответствующие ограничения.

  1. В файле Pages/Pizza.cshtml.cs примените следующие изменения:

    1. Добавьте атрибут [Authorize] к классу PizzaModel.

      [Authorize]
      public class PizzaModel : PageModel
      

      Атрибут описывает требования к проверке подлинности пользователя для страницы. В этом случае имеются лишь требования к прохождению пользователем проверки подлинности. Анонимные пользователи не могут просматривать страницу и перенаправляются на страницу входа.

    2. Разрешите ссылку на Authorize, добавив следующую строку в директивы using в верхней части файла:

      using Microsoft.AspNetCore.Authorization;
      
    3. Добавьте в класс PizzaModel следующее свойство:

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

      Приведенный выше код определяет, имеет ли прошедший проверку подлинности пользователь утверждение IsAdmin со значением True. Для обращения к результату этой оценки служит свойство IsAdmin, доступное только для чтения.

    4. Добавьте if (!IsAdmin) return Forbid(); в начало обоих методовOnPost и 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");
      }
      

      На следующем шаге вы скроете элементы пользовательского интерфейса создания и удаления для пользователей, не являющихся администраторами. Это не помешает злоумышленнику обращаться к этим конечным точкам напрямую с помощью таких инструментов как HttpRepl или Postman. Добавление этой проверки гарантирует, что при попытке такого обращения будет возвращен код состояния HTTP 403.

  2. В файл Pages/Pizza.cshtml добавьте проверки, чтобы скрыть элементы пользовательского интерфейса администратора от пользователей, не являющихся администраторами:

    Скрыть форму "Новая пицца"

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

    Скрыть кнопку "Удалить пиццу"

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

    В результате предыдущих изменений элементы пользовательского интерфейса, которые должны быть доступны только администраторам, будут отображаться только в том случае, если прошедший проверку подлинности пользователь является администратором.

Применение политики авторизации

Есть еще одна вещь, которую нужно заблокировать. Существует страница, которая должна быть доступна только администраторам. Это страница с соответствующим названием Pages/AdminsOnly.cshtml. Давайте создадим политику для проверки утверждения IsAdmin=True.

  1. В файле Program.cs внесите следующие изменения:

    1. Включите следующий выделенный код:

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

      Приведенный выше код определяет политику авторизации Admin. Она требует, чтобы пользователь прошел проверку подлинности и имел утверждение IsAdmin со значением True.

    2. Измените вызов AddRazorPages следующим образом:

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

      Вызов метода AuthorizePage защищает маршрут страницы Razor /AdminsOnly, применяя политику Admin. Пользователи, прошедшие проверку подлинности, которые не удовлетворяют требованиям политики, получают сообщение Access denied (В доступе отказано).

      Совет

      Вместо этого можно было бы внести изменения в файл AdminsOnly.cshtml.cs. В этом случае пришлось бы добавить [Authorize(Policy = "Admin")] в качестве атрибута в класс AdminsOnlyModel. Преимуществом этого варианта по сравнению с описанным выше подходом AuthorizePage является то, что не приходится вносить изменения в защищаемую страницу Razor. Вместо этого вносятся изменения в файл Program.cs для управления авторизацией.

  2. Внесите следующие изменения в файл Pages/Shared/_Layout.cshtml:

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

    Предыдущее изменение скрывает ссылку Администраторы в заголовке, если пользователь не является администратором.

Добавление утверждения IsAdmin для пользователя

Чтобы определить, какие пользователи должны получить утверждение IsAdmin=True, ваше приложение будет идентифицировать администратора с помощью подтвержденного адреса электронной почты.

  1. Добавьте выделенное свойство в файл appsettings.json:

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

    Это подтвержденный адрес электронной почты, который получает назначенное утверждение.

  2. В файле Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs внесите следующие изменения:

    1. Включите следующий выделенный код:

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

      Предыдущее изменение модифицирует конструктор, так чтобы он получал IConfiguration из контейнера IoC. IConfiguration содержит значения из appsettings.json и назначается свойству Configuration, доступному только для чтения.

    2. Внесите выделенные изменения в метод 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();
      }
      

      В предыдущем коде:

      • Строка AdminEmail считывается из свойства Configuration и назначается adminEmail.
      • Оператор ?? объединения значений NULL используется для обеспечения adminEmail того, что для параметра задано значение string.Empty , если в файле appsettings.json нет соответствующего значения.
      • Если адрес электронной почты пользователя успешно подтвержден:
        • Адрес пользователя сравнивается с adminEmail. Для сравнения без учета регистра используется метод string.Compare().
        • Метод AddClaimAsync класса UserManager вызывается для сохранения утверждения IsAdmin в таблице AspNetUserClaims.
    3. Добавьте следующий код в начало файла. Он разрешает ссылки на класс Claim в методе OnGetAsync:

      using System.Security.Claims;
      

Проверка утверждения администратора

Давайте выполним последний тест, чтобы проверить новые функции для администраторов.

  1. Убедитесь, что вы сохранили все изменения.

  2. Запустите приложение, выполнив команду dotnet run.

  3. Перейдите к приложению и войдите с помощью существующего пользователя, если вы еще не вошли в систему. Выберите Список пицц в заголовке. Обратите внимание, что для пользователя не отображаются элементы интерфейса, позволяющие создавать и удалять пиццы.

  4. В заголовке нет ссылки Администраторы. В адресной строке браузера перейдите напрямую к странице AdminsOnly. Замените /Pizza в URL-адресе на /AdminsOnly.

    Пользователю запрещено переходить на эту страницу. Отображается сообщение Access denied (В доступе отказано).

  5. Выберите Logout (Выйти).

  6. Зарегистрируйте нового пользователя с адресом admin@contosopizza.com.

  7. Как и раньше, подтвердите адрес электронной почты нового пользователя и выполните вход.

  8. Выполнив вход с новым пользователем с правами администратора, щелкните ссылку Список пицц в заголовке.

    Пользователь с правами администратора может создавать и удалять пиццы.

  9. Щелкните ссылку Администраторы в заголовке.

    Откроется страница AdminsOnly.

Просмотр таблицы AspNetUserClaims

Используя расширение SQL Server в 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

Откроется вкладка с результатами, похожими на следующие:

Адрес электронной почты ClaimType ClaimValue
admin@contosopizza.com IsAdmin Верно

Утверждение IsAdmin сохраняется как пара "ключ-значение" в таблице AspNetUserClaims. Запись AspNetUserClaims связана с записью пользователя в таблице AspNetUsers.

Сводка

В этом уроке вы изменили приложение для хранения утверждений и применения политик условного доступа.