Содержание
В предыдущей части мы разработали своб систему аутентификации пользователей с использованием библиотеки ASP.NET Core Identity. При этом, мы не затрагивали вопросов авторизации пользователя. В этой части мы продолжим работу над приложением и научимся авторизовывать пользователей, а также рассмотрим один из вариантов авторизации — авторизацию по ролям.
Авторизация пользователей в ASP.NET Core MVC
Для авторизации пользователя в ASP.NET Core MVC мы можем использовать атрибут [Authorize]
, который применяется к действиям контроллера или к контроллеру целиком. Например, в контроллере HomeController
у нас имеется действие Privacy
, которое отправляет нам одноименное представление. Применим к этому действию атрибут [Authorize]
[Authorize] public IActionResult Privacy() { return View(); }
Теперь запустим наше приложение и попробуем перейти по ссылке в приложении
В итоге, мы получим вот такую некрасивую страницу в браузере:
Здесь стоит сделать несколько пояснений. Во-первых, переход на эту страницу говорит нам о том, что атрибут [Authorize]
действительно защитил доступ к представлению (иначе, мы бы просто перешли по адресу). Во-вторых, в нашем приложении появился путь, которого мы нигде не задавали Identity/Account/Login?ReturnUrl=%2FHome%2FPrivacy
, параметр ReturnUrl
указывает ровно на тот путь по которому мы пытались перейти. Сам же путь Identity/Account/Login
— это путь по умолчанию, который используется ASP.NET Core Identity в качестве пути по которому пользователь должен осуществить вход — ввести логин и пароль. Но, так как у нас путь для входа совсем иной, а именно /account/signin
, то ASP.NET Core MVC справедливо выдает нам ошибку 404 Not Found.
Чтобы изменить путь по умолчанию для входа пользователя, перейдем в Program.cs и настроим куки для нашего приложения следующим образом:
builder.Services.ConfigureApplicationCookie(opts => opts.LoginPath = "/account/signin");
Теперь можно снова запустить приложение и убедиться, что вместо ошибки 404 при переходе на страницу Policy, мы попадает на страницу ввода логина и пароля.
Кроме атрибута [Authorize]
для защиты данных, мы также можем использовать противоположный по действию атрибут — [AllowAnonymous]
, который прямо указывает на то, что доступ к действию/контроллеру имеют все пользователи, включая и не аутентифицированных. Например, мы можем применить этот атрибут к действию в контроллере AccountController
при входе пользователя:
[Route("{area}/{action}")] [HttpPost] [AllowAnonymous] public async Task<IActionResult> SignIn(LoginModel model) { var result = await _signInManager.PasswordSignInAsync(model.Login, model.Password, false, lockoutOnFailure: false); if (result.Succeeded) { return LocalRedirect("/"); } else { ModelState.AddModelError("", $"Задан неверный логин или пароль"); } return View("Login"); }
Теперь, когда у нас заработала авторизация, можно переходить непосредственно, к управлению ролями пользователей и авторизации по ролям.
Роли пользователей в ASP.NET Core MVC
Добавление служб ролей в Identity
За работу с ролями пользователей в Identity отвечает сервис под названием RoleManager
. Для того, чтобы зарегистрировать этот сервис необходимо внести небольшие изменения в наш файл Program.cs:
builder.Services.AddDefaultIdentity<ApplicationUser>(options => { options.Password.RequireNonAlphanumeric = true; options.Password.RequireDigit = true; options.Password.RequiredLength = 10; }) .AddRoles<ApplicationRole>() //включаем поддержку ролей .AddEntityFrameworkStores<ApplicationContext>();
здесь мы добавили метод AddRoles()
, указав в качестве класса роли, созданный нами ранее класс ApplicationRole
, который выглядит следующим образом:
using Microsoft.AspNetCore.Identity; namespace AspMvcAuth.Models { public class ApplicationRole: IdentityRole<int> { } }
Добавление контроллера и представлений для управления ролями пользователей
По умолчанию, у нас нет никаких ролей в нашей системе аутентификации — мы их должны добавить самостоятельно. Для этого, создадим новый контроллер в области Account
с названием RoleController
:
Конструктор контроллера будет следующим:
RoleManager<ApplicationRole> _manager; UserManager<ApplicationUser> _users; public RoleController(RoleManager<ApplicationRole> manager, UserManager<ApplicationUser> users) { _manager = manager; _users = users; }
здесь мы запрашиваем сервис работы с ролями пользователей RoleManager<ApplicationRole>
и, для дальнейшей работы, сервис пользователей с которым мы уже немного знакомы. Сервис RoleManager
предоставляет нам следующие методы и свойства для работы:
CreateAsync () |
Создает новую роль |
DeleteAsync () |
Удаляет указанную роль |
FindByIdAsync () |
Находит роль по ее идентификатору |
FindByNameAsync() |
Находит роль по ее названию |
RoleExistsAsync() |
Используется для проверки, существует ли роль с указанным именем или нет |
UpdateAsync() |
Обновляет роль |
Roles |
Свойство возвращающее все роли в Identity |
Каждый пользователь может иметь одну, несколько или вообще не иметь никаких ролей. Управление ролями конкретного пользователя осуществляется с использованием сервиса UserManager
у которого определены следующие методы:
Add |
Добавляет пользователя в указанную роль |
Add |
Добавляет пользователя в указанный список ролей |
Get |
Возвращает список имен ролей, к которому принадлежит указанный пользователь |
Get |
Возвращает список пользователей, которые добавлены в указанную роль |
Is |
Возвращает флаг, указывающий, является ли указанный пользователь членом заданной роли. |
Remove |
Удаляет пользователя из роли. |
Remove |
Удаляет пользователя из заданного списка ролей. |
Для демонстрации работы с этими методами, нам потребуется создать два представления:
- для вывода списка ролей. В этом представлении необходимо предусмотреть переход к редактированию роли, а также выполнение такого действия, как удаление роли из хранилища;
- для редактирования роли — изменения названия роли, добавления/удаления пользователя из роли
В начале рассмотрим представление для вывода списка ролей и напишем необходимые действия контроллера.
Представление для вывода списка ролей
Добавим новое представление в папку Areas/Account/Views и назовем его Role.cshtml
:
Представление будет иметь следующий код:
@using AspMvcAuth.Models @model IEnumerable<ApplicationRole> @addTagHelper AspMvcAuth.TagHelpers.*, AspMvcAuth <h2>Роли пользователей</h2> <form class="row g-3" method="post"> <div class="col-auto"> <input type="text" readonly class="form-control-plaintext" id="roleLabel" value="Роль"> </div> <div class="col-auto"> <input type="text" class="form-control" name="roleName" id="roleName" placeholder="Название роли..."> </div> <div class="col-auto"> <button type="submit" class="btn btn-primary mb-3">Добавить</button> </div> </form> <table class="table"> <thead> <tr> <th scope="col">ID</th> <th scope="col">Имя</th> <th scope="col">Пользователи</th> <th scope="col">Редактировать</th> <th scope="col">Удалить</th> </tr> </thead> <tbody> @foreach (var item in Model) { <tr> <td>@item.Id</td> <td>@item.Name</td> <td u-role="@item.Id"></td> <td> <a class="btn btn-sm btn-secondary" asp-action="Update" asp-route-id="@item.Id">Update</a> </td> <td> <form asp-action="Delete" asp-route-id="@item.Id" method="post"> <button type="submit" class="btn btn-sm btn-danger"> Delete </button> </form> </td> </tr> } </tbody> </table>
Рассмотрим код представление более подробно. В качестве модели представления выступает список ролей:
@model IEnumerable<ApplicationRole>
В представлении создана форма для добавления новой роли в хранилище:
<h2>Роли пользователей</h2> <form class="row g-3" method="post"> <div class="col-auto"> <input type="text" readonly class="form-control-plaintext" id="roleLabel" value="Роль"> </div> <div class="col-auto"> <input type="text" class="form-control" name="roleName" id="roleName" placeholder="Название роли..."> </div> <div class="col-auto"> <button type="submit" class="btn btn-primary mb-3">Добавить</button> </div> </form>
а также таблица, в которой выводится список всех ролей, а также кнопки для управления конкретной ролью:
<table class="table"> <thead> <tr> <th scope="col">ID</th> <th scope="col">Имя</th> <th scope="col">Пользователи</th> <th scope="col">Редактировать</th> <th scope="col">Удалить</th> </tr> </thead> <tbody> @foreach (var item in Model) { <tr> <td>@item.Id</td> <td>@item.Name</td> <td u-role="@item.Id"></td> <td> <a class="btn btn-sm btn-secondary" asp-action="Update" asp-route-id="@item.Id">Update</a> </td> <td> <form asp-action="Delete" asp-route-id="@item.Id" method="post"> <button type="submit" class="btn btn-sm btn-danger"> Delete </button> </form> </td> </tr> } </tbody> </table>
В этой таблице стоил обратить внимание на атрибут u-role
, который используется для элементов <td>
таблицы:
<td u-role="@item.Id"></td>
Для использования этого атрибута был создан tag-хэлпер RoleTagHelper
, который разместили в папке TagHelpers:
Этот tag-хэлпер используется для вывода списка пользователей, которые были добавлены в определенную роль и выглядит следующим образом:
using AspMvcAuth.Models; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Razor.TagHelpers; namespace AspMvcAuth.TagHelpers { [HtmlTargetElement("td", Attributes = "u-role")] public class RoleTagHelper: TagHelper { private readonly UserManager<ApplicationUser> userManager; private readonly RoleManager<ApplicationRole> roleManager; [HtmlAttributeName("u-role")] public string Role { get; set; } public RoleTagHelper(UserManager<ApplicationUser> userManager, RoleManager<ApplicationRole> roleManager) { this.userManager = userManager; this.roleManager = roleManager; } public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { List<string?> users = []; ApplicationRole role = await roleManager.FindByIdAsync(Role); if (role != null) { users = (await userManager.GetUsersInRoleAsync(role.Name)) .Select((user) => user.UserName) .ToList(); } output.Content.SetContent(users.Count == 0 ? "Нет пользователей" : string.Join(", ", users)); } } }
Для формирования списка пользователей, входящих в определенную роль, в tag-хэлпере передается идентификатор роли:
[HtmlAttributeName("u-role")] public string Role { get; set; }
по id
роли мы ищем роль в хранилище:
ApplicationRole role = await roleManager.FindByIdAsync(Role);
и, затем, обращаемся к менеджеру пользователей, чтобы получить список имен пользователей, которые были добавлены в определенную роль:
users = (await userManager.GetUsersInRoleAsync(role.Name)) .Select((user) => user.UserName) .ToList();
Что касается редактирования и удаления роли, в представлении Role.cshtml предусмотрены две кнопки:
<td> <a class="btn btn-sm btn-secondary" asp-action="Update" asp-route-id="@item.Id">Update</a> </td> <td> <form asp-action="Delete" asp-route-id="@item.Id" method="post"> <button type="submit" class="btn btn-sm btn-danger"> Delete </button> </form> </td>
Действия контроллера для работы с ролями пользователей
Действия чтения и создания роли
Для чтения списка ролей и создания новой роли в контроллере предусмотрено два действия:
[Route("{area}/{action}")] [HttpGet] public IActionResult Role() { return View(_manager.Roles); } [Route("{area}/{action}")] [HttpPost] public IActionResult Role([FromForm]string roleName) { _manager.CreateAsync(new ApplicationRole() { Name = roleName }); return RedirectToAction("Role"); }
Первый метод Role()
обрабатывает GET-запрос и отправляет в представление список ролей, содержащихся в хранилище. Второй метод Role()
обрабатывает POST-запросы и предназначен для создания новой роли в хранилище. Для создания роли в метод из формы представления передается название новой роли. После того, как новая роль добавлены мы осуществляем редирект на первый метод Role()
:
return RedirectToAction("Role");
Чтобы загрузить обновленный список ролей.
Действие для удаления роли из хранилища
Для удаления роли из хранилища используется следующее действие контроллера:
[Route("{area}/{action}/{id}")] [HttpPost] public async Task<IActionResult> Delete(string id) { var role = await _manager.FindByIdAsync(id); if (role != null) await _manager.DeleteAsync(role); return RedirectToAction("Role"); }
в представлении это действие вызывается кликом по кнопке «Delete»:
<td> <form asp-action="Delete" asp-route-id="@item.Id" method="post"> <button type="submit" class="btn btn-sm btn-danger"> Delete </button> </form> </td>
Для редактирования роли, а также для добавления/удаления пользователей из роли используется отдельное представление. Рассмотрим его, а также соответствующие действия контроллера для работы с ролями пользователей.
Представление для управления ролями пользователей
Представление для управления ролями пользователей необходимо размещать также в папке Areas/Account/Views/Role. В нашем приложении это представление будет называться Update.cshtml:
Код представления следующий:
@using AspMvcAuth.Models @model RoleEdit <h2>Обновление роли</h2> <form method="post"> <input type="hidden" name="roleId" value="@Model.Role.Id" /> <h2 class="bg-info p-1 text-white">Изменить название</h2> <input asp-for="@Model.Role.Name" type="text" class="form-control p-1" name="roleName" id="roleName" placeholder="Название роли..."> <h2 class="bg-info p-1 text-white">Добавить в @Model.Role.Name</h2> <table class="table table-bordered table-sm"> @if (Model.NonMembers.Count() == 0) { <tr><td colspan="2">Некого добавлять</td></tr> } else { @foreach (ApplicationUser user in Model.NonMembers) { <tr> <td>@user.UserName</td> <td> <input type="checkbox" name="AddIds" value="@user.Id"> </td> </tr> } } </table> <h2 class="bg-info p-1 text-white">Удалить из @Model.Role.Name</h2> <table class="table table-bordered table-sm"> @if (Model.Members.Count() == 0) { <tr><td colspan="2">Некого удалять</td></tr> } else { @foreach (ApplicationUser user in Model.Members) { <tr> <td>@user.UserName</td> <td> <input type="checkbox" name="DeleteIds" value="@user.Id"> </td> </tr> } } </table> <button type="submit" class="btn btn-primary">Сохранить</button> </form>
Как можно увидеть, это представление использует модель, которую мы ранее не создавали:
@model RoleEdit
Эта модель содержит данные, необходимые как для редактирования самой роли, так и информацию о пользователях, включенных и не включенных в эту роль. Модель выглядит следующим образом:
namespace AspMvcAuth.Models { public class RoleEdit { public ApplicationRole Role { get; set; } public IEnumerable<ApplicationUser> Members { get; set; } public IEnumerable<ApplicationUser> NonMembers { get; set; } } }
и размещается в папке Models приложения. В представлении для управления ролью используется форма, в которой, на основании данных модели, формируется два списка:
1. Список пользователей, которых необходимо включить в роль:
<table class="table table-bordered table-sm"> @if (Model.NonMembers.Count() == 0) { <tr><td colspan="2">Некого добавлять</td></tr> } else { @foreach (ApplicationUser user in Model.NonMembers) { <tr> <td>@user.UserName</td> <td> <input type="checkbox" name="AddIds" value="@user.Id"> </td> </tr> } } </table>
2. Список пользователей, которых необходимо исключить из роли:
<table class="table table-bordered table-sm"> @if (Model.Members.Count() == 0) { <tr><td colspan="2">Некого удалять</td></tr> } else { @foreach (ApplicationUser user in Model.Members) { <tr> <td>@user.UserName</td> <td> <input type="checkbox" name="DeleteIds" value="@user.Id"> </td> </tr> } } </table>
Чтобы при отправке формы сохранить информацию о том, какая из ролей подвергается изменению, в форме предусмотрены два текстовых поля:
<input type="hidden" name="roleId" value="@Model.Role.Id" /> <input asp-for="@Model.Role.Name" type="text" class="form-control p-1" name="roleName" id="roleName" placeholder="Название роли...">
при этом, поле roleId
является скрытым. Для того, чтобы передавать данные формы в удобном для использования виде, в приложении предусмотрена следующая модель:
using System.ComponentModel.DataAnnotations; namespace AspMvcAuth.Models { public class RoleModification { [Required] public string RoleName { get; set; } public string RoleId { get; set; } public string[]? AddIds { get; set; } public string[]? DeleteIds { get; set; } } }
Теперь рассмотрим действия контроллера для редактирования роли.
Действия контроллера для редактирования роли
Как и в случае с чтением списка ролей, в контроллере предусмотрена пара методов Update()
, один из которых обрабатывает GET-запрос и предназначен, фактически, для загрузки представления, а второй — обрабатывает POST-запрос для непосредственного редактирования роли:
[Route("{area}/{action}/{id}")] [HttpGet] public async Task<IActionResult> Update(string id) { ApplicationRole role = await _manager.FindByIdAsync(id); List<ApplicationUser> members = new List<ApplicationUser>(); List<ApplicationUser> nonMembers = new List<ApplicationUser>(); foreach (ApplicationUser user in _users.Users) { var list = await _users.IsInRoleAsync(user, role.Name) ? members : nonMembers; list.Add(user); } return View(new RoleEdit { Role = role, Members = members, NonMembers = nonMembers }); } [Route("{area}/{action}/{id}")] [HttpPost] public async Task<IActionResult> Update([FromForm] RoleModification model) { var role = await _manager.FindByIdAsync(model.RoleId); role.Name = model.RoleName; await _manager.UpdateAsync(role); IdentityResult result; if (ModelState.IsValid) { foreach (string userId in model.AddIds ?? new string[] { }) { ApplicationUser user = await _users.FindByIdAsync(userId); if (user != null) { result = await _users.AddToRoleAsync(user, model.RoleName); if (!result.Succeeded) return BadRequest(result); } } foreach (string userId in model.DeleteIds ?? new string[] { }) { ApplicationUser user = await _users.FindByIdAsync(userId); if (user != null) { result = await _users.RemoveFromRoleAsync(user, model.RoleName); if (!result.Succeeded) return BadRequest(result); } } } return RedirectToAction("Role"); }
В методе, обрабатывающем GET-запрос, формируется объект модели RoleEdit, для чего загружается список пользователей и каждый пользователь проверяется на вхождение в заданную роль:
ApplicationRole role = await _manager.FindByIdAsync(id); //ищем роль по её ID List<ApplicationUser> members = new List<ApplicationUser>(); List<ApplicationUser> nonMembers = new List<ApplicationUser>(); foreach (ApplicationUser user in _users.Users) //перебираем всех пользователей { //выбираем список в который необходимо поместить пользователя var list = await _users.IsInRoleAsync(user, role.Name) ? members : nonMembers; //добавляем пользователя в список list.Add(user); }
после того, как списки сформированы, отправляем модель в представление:
return View(new RoleEdit { Role = role, Members = members, NonMembers = nonMembers });
Второй метод немногим сложнее. В этом методе мы получаем в качестве параметра объект модели RoleModification
из формы и выполняем два цикла — первый цикл ищет в хранилище конкретного пользователя и добавляет его к роли:
foreach (string userId in model.AddIds ?? new string[] { }) { ApplicationUser user = await _users.FindByIdAsync(userId); if (user != null) { result = await _users.AddToRoleAsync(user, model.RoleName); if (!result.Succeeded) return BadRequest(result); } }
второй цикл аналогичный, но действует в обратном ключе — удаляет найденного пользователя из роли. После редактирования роли мы возвращаемся к списку ролей:
return RedirectToAction("Role");
Осталось только добавить ссылку для доступа к списку ролей из нашего приложения. Для этого воспользуемся макетом приложения (Файл _Layout.cshtml) в которй добавим следующий tag-хэлпер ссылки:
<a class="nav-link text-dark" asp-area="Account" asp-controller="Role" asp-action="Role">Управление ролями</a>
Проверим работу приложения.
Проверка работы приложения с ролями пользователей
Для примера, добавим в приложение новые роли. Для этого перейдем по новой ссылке:
и воспользуемся формой для добавления ролей в хранилище. На рисунке, для примера показаны три роли, добавленные в хранилище:
Обновим роль ADMIN. Нажав на кнопку Update мы перейдем к одноименному представлению:
Так как пока нет никаких пользователей с этой ролью, то второй список «Удалить из ADMIN» пустой, а в первом — содержится единственный пользователь системы, которого мы зарегистрировали в предыдущей части. Выберем пользователя и нажмем кнопку «Сохранить»:
В результате, список ролей пользователей будет выглядеть следующим образом:
Можно добавить пользователя во все три роли. Или зарегистрировать ещё несколько пользователей и назначить им роли и т.д. Мы этого делать не будем, а перейдем к следующей части работы — настроим и проверим авторизацию пользователей по ролям.
Авторизация пользователей по ролям
Для того, чтобы дать доступ к какому-то ресурсу вашего приложения пользователю с определенной ролью, также используется атрибут [Authorize]
в параметрах которого указываются необходимые роли. Например, вернемся к нашему «домашнему» контроллеру HomeController
и применим к действию Privacy()
следующий атрибут:
[Authorize(Roles ="MANAGER")] public IActionResult Privacy() { return View(); }
Теперь доступ к представлению Privacy будут иметь только пользователи, включенные в роль MANAGER. Попробуем зайти в приложение и получить доступ к этому приложению. Напомню, что единственный пользователь системы у нас входит в роль ADMIN, а не MANAGER.
При попытке перейти на страницу Privacy() мы получим ошибку:
Здесь, опять де, как и в самом начале, ASP.NET Core MVC пытается нас перенаправить на адрес по умолчанию, который используется для указания пользователю того, что ему запрещен доступ к этой странице. Чтобы переопределить этот путь и сделать наше приложение чуть более информативным для пользователя, давайте настроим новый путь к странице с информацией о запрете доступа. Делается это также в настройках куки приложения следующим образом:
builder.Services.ConfigureApplicationCookie(opts => { opts.LoginPath = "/account/signin"; opts.AccessDeniedPath = "/AccessDanied"; //путь к странице с информацией о запрете доступа });
Теперь добавим в приложение новое представление с названием AccessDanied.cshtml
, разместив его в папке Views/Shared. Код представления:
<h2 class="bg-danger p-1 text-white">Вам запрещен доступ к этой странице</h2>
В контроллер HomeController добавим действие, возвращающее это представление:
[Authorize] [Route("/AccessDanied")] public IActionResult AccessDanied() { return View(); }
Теперь, если пользователь аутентифицирован (вошел в систему), но не авторизован (не имеет необходимой роли), то ему будет загружаться представление AccessDanied, а если пользователь не аутентифицирован, то ему будет предложено войти в систему. Вот как будет выглядеть запрет доступа для пользователя:
Итого
В этой части мы разобрались с механизмом авторизации пользователей по ролям с использованием ASP.NET Core Identity и разработали приложение в котором научились работать в ролями пользователей, назначать и удалять пользователей из ролей. Для выполнения этих задач мы разработали новый контроллер, а также вспомогательный tag-хэлпер и необходимые представления, включая и представление, информирующее аутентифицированного пользователя о запрете доступа к определенному ресурсу.
P.S.
Эта статья в части изложения далась мне довольно не просто, так как оказалось довольно трудно выстроить логику изложения материала, когда надо постоянно переключаться с описания контроллера, на представление и наоборот и, дополнительно, не забывать про используемые модели. Поэтому, уважаемые читатели, если вдруг изложенный материал «не зайдет» или вы в чем-то запутаетесь, то, на всякий случай, весь код этого проекта я буду отправлять в репозиторий на GitHub:
Скачать код проекта из Githubтолько при использовании кода проекта, пожалуйста, не забывайте, что для строки подключения к БД используются секреты пользователей. Поэтому, прежде, чем запустить проект либо создайте необходимый секрет, либо измените файл Program.cs, чтобы получать настройки подключения к БД из appsettings.json.