Упражнение. Использование утверждений с авторизацией на основе политик
В предыдущем уроке вы узнали о различии между проверкой подлинности и авторизацией. Вы также узнали, как утверждения используются политиками для авторизации. В этом уроке вы используете удостоверение для хранения утверждений и применения политик для условного доступа.
Защита страницы "Список пицц"
Вы получили новое требование о том, что страница "Список пицц" должна отображаться только для пользователей, прошедших проверку подлинности. Кроме того, возможность создавать и удалять пиццы должна быть только у администраторов. Давайте реализуем соответствующие ограничения.
В файле Pages/Pizza.cshtml.cs примените следующие изменения:
Добавьте атрибут
[Authorize]
к классуPizzaModel
.[Authorize] public class PizzaModel : PageModel
Атрибут описывает требования к проверке подлинности пользователя для страницы. В этом случае имеются лишь требования к прохождению пользователем проверки подлинности. Анонимные пользователи не могут просматривать страницу и перенаправляются на страницу входа.
Разрешите ссылку на
Authorize
, добавив следующую строку в директивыusing
в верхней части файла:using Microsoft.AspNetCore.Authorization;
Добавьте в класс
PizzaModel
следующее свойство:[Authorize] public class PizzaModel : PageModel { public bool IsAdmin => HttpContext.User.HasClaim("IsAdmin", bool.TrueString); public List<Pizza> pizzas = new();
Приведенный выше код определяет, имеет ли прошедший проверку подлинности пользователь утверждение
IsAdmin
со значениемTrue
. Для обращения к результату этой оценки служит свойствоIsAdmin
, доступное только для чтения.Добавьте
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 или curl. Добавление этой проверки гарантирует, что при попытке такого обращения будет возвращен код состояния HTTP 403.
В файл 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
.
В Program.cs внесите следующие изменения:
Включите следующий выделенный код:
// 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
.Измените вызов
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 для управления авторизацией.
Внесите следующие изменения в файл 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
, ваше приложение будет идентифицировать администратора с помощью подтвержденного адреса электронной почты.
Добавьте выделенное свойство в файл appsettings.json:
{ "AdminEmail" : "admin@contosopizza.com", "Logging": {
Это подтвержденный адрес электронной почты, который получает назначенное утверждение.
В файле Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs внесите следующие изменения:
Включите следующий выделенный код:
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
, доступному только для чтения.Внесите выделенные изменения в метод
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
.
- Адрес пользователя сравнивается с
- Строка
Добавьте следующий код в начало файла. Он разрешает ссылки на класс
Claim
в методеOnGetAsync
:using System.Security.Claims;
Проверка утверждения администратора
Давайте выполним последний тест, чтобы проверить новые функции для администраторов.
Убедитесь, что вы сохранили все изменения.
Запустите приложение, выполнив команду
dotnet run
.Перейдите к приложению и войдите с помощью существующего пользователя, если вы еще не вошли в систему. Выберите Список пицц в заголовке. Обратите внимание, что для пользователя не отображаются элементы интерфейса, позволяющие создавать и удалять пиццы.
В заголовке нет ссылки Администраторы. В адресной строке браузера перейдите напрямую к странице AdminsOnly. Замените
/Pizza
в URL-адресе на/AdminsOnly
.Пользователю запрещено переходить на эту страницу. Отображается сообщение Access denied (В доступе отказано).
Выберите Logout (Выйти).
Зарегистрируйте нового пользователя с адресом
admin@contosopizza.com
.Как и раньше, подтвердите адрес электронной почты нового пользователя и войдите в систему.
После входа с помощью нового администратора выберите ссылку "Список пиццы" в заголовке.
Пользователь с правами администратора может создавать и удалять пиццы.
Выберите ссылку "Администраторы " в заголовке.
Откроется страница 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
.
Итоги
В этом уроке вы изменили приложение для хранения утверждений и применения политик условного доступа.