Содержание
В предыдущей части мы разработали своб систему аутентификации пользователей с использованием библиотеки 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.









