Содержание
На данный момент у нас в проекте имеется контроллер для работы с учётными записями пользователей и их аутентификации, а также контроллер WeatherForecastController
, который создается по умолчанию в шаблонном проекте ASP.NET Core Web API. Допустим, мы хотим позволить получать данные о прогнозе погоду только авторизованным пользователям. В ASP.NET Core сделать это достаточно просто, используя специальный атрибут [Authorize]
.
Откроем исходный код контроллера WeatherForecastController
и добавим к действию Get() атрибут [Authorize]
[HttpGet] [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] public IEnumerable<WeatherForecast> Get() { return Enumerable.Range(1, 5).Select(index => new WeatherForecast { Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), TemperatureC = Random.Shared.Next(-20, 55), Summary = Summaries[Random.Shared.Next(Summaries.Length)] }) .ToArray(); }
Здесь мы указали для атрибута только схему аутентификации — с использованием JWT-токенов. Другие параметры атрибута и их работу мы рассмотрим далее. Проверим действие этого атрибута. Для этого выполним уже имеющийся в http-файле проекта запрос на получение данных о прогнозе погоды (этот запрос также создается по умолчанию в шаблонном проекте). Если мы попытаемся выполнить этот запрос, то сервер ответит следующей ошибкой
Чтобы запрос был выполнен успешно нам необходимо:
- Выполнить аутентификацию пользователя и получить JWT-токен.
- Передать токен в заголовке запроса на добавление нового проекта.
Добавим в http-файл новый запрос
@jwt = token GET {{WebApplication6_HostAddress}}/weatherforecast/ Accept: application/json Authorization: Bearer {{jwt}}
Переменная jwt
будет хранить полученный при авторизации JWT-токен, который будет вставляться в заголовок Authorization
. Теперь выполним аутентификацию пользователя, используя созданный ранее запрос и зададим значение переменной jwt
И выполним запрос к контроллеру WeatherForecastController
Как можно видеть, при отправке JWT-токена в заголовках запроса действие контроллера успешно выполняется. Теперь, организовав процесс аутентификации и авторизации пользователей, мы можем далее развивать механизм авторизации пользователей нашего Web API. И первое обновление будет касаться одной из популярных схем авторизации пользователей – авторизации по ролям.
Авторизация по ролям
Достаточно часто для доступа к какой-либо закрытой информации требуется не только наличие самого факта аутентификации и JWT-токена, но и принадлежность пользователя к какой-либо группе (роли). Например, внутри организации мы можем выделить следующие роли пользователей:
- Сотрудник
- Менеджер
- Руководитель
Используя эти роли, мы можем разрешать или запрещать работу пользователя с определенными методами API. Например, пользователь со статусом «Сотрудник» может получат доступ только к чтению списка проектов организации, «Менеджер» — читать, добавлять и редактировать проекты, а «Руководитель» – получать доступ ко всем методам API. Также можно организовать работу приложения таким образом, чтобы пользователь мог принадлежать сразу нескольким ролям. Например, «Менеджер», как и любой другой работник организации, может быть её «Сотрудником», а руководитель принадлежать всем ролям в приложении.
Для работы с ролями пользователей в ASP.NET Core Identity предназначен отдельный сервис — RoleManager<TRole>
где TRole
– это класс, определяющий роли пользователей в приложении. По умолчанию, в ASP.NET Core Identity определен класс роли IdentityRole
, который мы можем использовать в своих приложениях.
Чтобы добавить роли пользователей в приложении вы можете действовать двумя способами:
- Добавить необходимые роли, используя миграции EF Core. Такой вариант, в основном, предполагает, что все роли пользователей будут определены один раз – в момент применения миграции и пользователи не будут иметь возможность их каким-либо образом изменять. Такой способ является наиболее простым в реализации.
- Организовать работу с ролями, используя сервис
RoleManager
, определив необходимые действия в контроллере. В этом случае можно позволить пользователям самим определять необходимые роли в приложение, изменять их и так далее.
Рассмотрим оба этих варианта. Начнем с первого варианта – использование миграции.
Добавление ролей по умолчанию
Чтобы добавить роли по умолчанию в наш проект, создадим в нашем проекте новую папку Configuration и разместим в ней класс создадим класс RoleConfiguration
со следующим содержимым:
using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore; namespace WebApplication6.Configuration { public class RoleConfiguration : IEntityTypeConfiguration<IdentityRole<int>> { public void Configure(EntityTypeBuilder<IdentityRole<int>> builder) { builder.HasData( new IdentityRole("Employee") { NormalizedName = "EMPLOYEE" }, new IdentityRole("Manager") { NormalizedName = "MANAGER" }, new IdentityRole("Chief") { NormalizedName = "CHIEF" } ); } } }
Этот класс реализует интерфейс IEntityTypeConfiguration<IdentityRole<int>>. В методе Configure() мы добавляем в хранилище три роли — Employee, Manager и Chief. Теперь изменим исходный код контекста базы данных (класс ApplicationContext
)
public class ApplicationContext : IdentityDbContext<User, IdentityRole<int>, int> { public ApplicationContext(DbContextOptions<ApplicationContext> options) : base(options) { Database.Migrate(); } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.ApplyConfiguration(new RoleConfiguration()); } }
Добавим в проект новую миграцию, выполнив команду:
PM> Add-Migration AddDefaultRoles
Add-Migration
возникает ошибка, то попробуйте перед выполнением команды закомментировать вызов метода Database.Migrate()
в конструкторе класса ApplicationContext
В контексте БД мы переопределили метод OnModelCreating()
в котором применяем созданный нами класс RoleConfiguration
. Благодаря такому подходу мы можем наполнять базу данных начальными данными, размещая добавляемые данные в отдельных классах. Но это уже вопросы использования EF Core, а не ASP.NET Core, поэтому не будем в них углубляться, а запустим приложение и выполним любой запрос к нашему API, чтобы был задействован контекст БД и применялась последняя миграция. После этого можете открыть файл базы данных и убедиться, что в таблице AspNetRoles появились новые роли:
Теперь мы можем использовать эти роли в приложении.
Регистрация пользователя с ролью по умолчанию
Для того, чтобы использовать авторизацию по ролям, наш пользователь должен принадлежать к какой-либо из ролей. Например, изменим действие регистрации пользователя в контроллере UsersController
следующим образом:
[HttpPost] public async Task<ActionResult> AddUser(UserDto user) { var data = await _userManager.FindByEmailAsync(user.Email); if (data != null) { ModelState.AddModelError("User", $"Пользователь с Email {user.Email} уже зарегистрирован в приложении"); return ValidationProblem(statusCode: 400, modelStateDictionary: ModelState ); } var userForReg = new User() { Email = user.Email, FirstName = user.FirstName, LastName = user.LastName, PhoneNumber = user.PhoneNumber, UserName = user.UserName, }; var result = await _userManager.CreateAsync(userForReg, user.Password); //добавляем пользователя к роли Employee if (result.Succeeded) { await _userManager.AddToRoleAsync(userForReg, "Employee"); } if (result.Succeeded) { return Created(); } else { foreach (var item in result.Errors) { ModelState.AddModelError(item.Code, item.Description); } return ValidationProblem(statusCode: 400, modelStateDictionary: ModelState); } }
Здесь мы добавили новое действие после того, как пользователь добавлен в хранилище:
//добавляем пользователя к роли Employee if (result.Succeeded) { await _userManager.AddToRoleAsync(userForReg, "Employee"); }
В результате, каждый новый пользователь по умолчанию будет принадлежать к роли Employee
. Теперь добавим нового пользователя в хранилище, используя ранее разработанный запрос к API. Например, я выполнил следующий запрос:
POST {{WebApplication6_HostAddress}}/users/ Content-Type: application/json { "UserName": "Test", "Email": "Test@csharp.webdelphi.ru", "FirstName": "Test", "LastName": "Test", "Password": "_12345q67890", "ConfirmPassword":"_12345q67890", "PhoneNumber": "1234" }
Если теперь выполнить запрос на добавление нового пользователя, то помимо записи пользователя в таблице AspNetUsers, также появится новая запись в таблице AspNetUserRoles, то есть пользователь будет добавлен к роли.
Передача ролей пользователя в JWT-токене
Теперь при аутентификации пользователя мы должны передать информацию о его ролях в JWT-токене. Для этого также используются объекты Claim
. Чтобы передать роли пользователя в JWT-токене немного изменим метод контроллера UsersController
GetUserClaims()
:
public async Task<List<Claim>> GetUserClaims() { var claims = new List<Claim>(); var roles = await _userManager.GetRolesAsync(_user); foreach (var role in roles) { claims.Add(new(ClaimTypes.Role, role)); } claims.Add(new(ClaimTypes.Name, _user.UserName)); return claims; }
В этом методе мы считываем все роли пользователя, используя метод GetRolesAsync()
. Так как этот метод асинхронный, то тип возвращаемого результата методом GetUserClaims()
мы также изменили с List<Claim>
на Task<List<Claim>>
. Полученные роли мы записываем в список List<Claim>
, используя обычный цикл foreach
и, указывая для каждого объекта Claim
тип ClaimTypes.Role
.
Так как метод GetUserClaims()
также стал асинхронным, то необходимо внести небольшие изменения в метод GenerateToken()
public async Task<string> GenerateToken() { var claims = await GetUserClaims(); var options = GetTokenOptions(claims); return new JwtSecurityTokenHandler().WriteToken(options); }
и действие LoginUser()
[HttpPost("login")] public async Task<IActionResult> LoginUser(LoginUserDto loginUser) { if (await ValidUser(loginUser)) { return Ok(await GenerateToken()); } return Unauthorized(); }
Теперь токен помимо имени пользователя будет также содержать информацию о ролях, что позволит нам использовать эту информацию для авторизации пользователя.
Действие котроллера с авторизацией по ролям
Остается использовать роли для авторизации пользователей в приложении. Изменим атрибут [Authorize] действия Get() контроллера WeatherForecastController следующим образом:
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme, Roles = "Employee")] public IEnumerable<WeatherForecast> Get()
Здесь мы добавили у атрибута [Authorize]
свойство Roles
в котором определили роль. Если необходимо определить в этом свойстве несколько ролей, то они перечисляются через запятую. Теперь только те пользователи, которые имеют одну или несколько из перечисленных ролей, смогут получить доступ к действию.
Проверка авторизации по ролям
Чтобы проверить авторизацию по ролям, нам необходимо:
- Аутентифицировать пользователя
- Вставить полученный jwt-токен в заголовок запроса на получение списка всех проектов.
Аутентифицируем нового пользователя, у которого мы определили роль EMPLOYEE, выполнив следующий запрос
POST {{WebApplication6_HostAddress}}/users/login/ Content-Type: application/json { "UserName": "Test", "Password": "_12345q67890" }
Полученный в ответе сервера JWT-токен необходимо вставить в переменную запроса на получения прогноза погоды, который мы добавили в http-файл выше. В случае, если у пользователя не определена требуемая роль, то в ответ вы получите код статуса 403 Forbidden
Таким образом, теперь мы можем определять для новых пользователей их роли и использовать в приложении авторизацию по ролям, определив для действий контроллеров атрибуты [Authorize]
с необходимыми значениями свойств Roles
.
На этом тема работы с ролями пользователей не заканчивается. Рассмотрим также вопросы, связанные с управлением ролями пользователей в ASP.NET Core Identity.
Управление ролями пользователей
Вполне возможно, что для работы может оказаться недостаточным использования ролей по умолчанию и вам будет необходимо обеспечить в приложении возможность создания собственных ролей, удаление или, наоборот, добавление новых ролей для пользователя и так далее. ASP.NET Core Identity позволяет реализовать подобные возможности, используя сервис RoleManager<TRole>
. Чтобы продемонстрировать работу с этим сервисом добавим в проект новый контроллер RolesController
[Route("[controller]")] [ApiController] public class RolesController : ControllerBase { private RoleManager<IdentityRole<int>> _roleManager; public RolesController(RoleManager<IdentityRole<int>> roleManager) { _roleManager = roleManager; } } }
У сервиса RoleManager<TRole>
определены следующие методы, которые мы можем использовать для управления ролями пользователей в приложении
Метод | Возвращаемое значение | Описание |
public virtual Task<IdentityResult> CreateAsync (TRole role); |
Task<IdentityResult> |
Создает в хранилище новую роль, указанную в параметре role |
public virtual Task<IdentityResult> DeleteAsync (TRole role); |
Task<IdentityResult> |
Удаляет из хранилища роль, указанную в параметре role |
public virtual Task<TRole?> FindByIdAsync (string roleId); |
Task<TRole> |
Возвращает из хранилища роль с указанным в параметре roleId идентификатором или null , если роль не найдена |
public virtual Task<TRole?> FindByNameAsync (string roleName); |
Task<TRole> |
Возвращает из хранилища роль с указанным в параметре roleName именем или null , если роль не найдена |
public virtual Task<string> GetRoleIdAsync (TRole role); |
Task<String> |
Возвращает идентификатор роли, объект которой указан в параметре role |
public virtual Task< IdentityResult> UpdateAsync (TRole role); |
Task<IdentityResult> |
Обновляет в хранилище роль, объект которой указан в параметре role |
Воспользуемся этими методами в нашем контроллере, добавив следующие действия:
[HttpGet] public List<IdentityRole<int>> GetRoles() { return [.. _roleManager.Roles]; } [HttpPost] public async Task<IActionResult> PostRole(IdentityRole<int> role) { var result = await _roleManager.CreateAsync(role); return Response(result); } [HttpPut("{id}")] public async Task<IActionResult> UpdateRole(string id, IdentityRole role) { var updatedRole = await _roleManager.FindByIdAsync(id); if (updatedRole == null) { ModelState.AddModelError("NotFound", $"Роль с Id = {id} не найдена в хранилище"); return ValidationProblem(ModelState); } updatedRole.Name = role.Name; updatedRole.NormalizedName = role.NormalizedName; var result = await _roleManager.UpdateAsync(updatedRole); return Response(result); } [HttpDelete("{id}")] public async Task<IActionResult> DeleteRole(string id) { var role = await _roleManager.FindByIdAsync(id); if (role == null) { ModelState.AddModelError("NotFound", $"Роль с Id = {id} не найдена в хранилище"); return ValidationProblem(ModelState); } var result = await _roleManager.DeleteAsync(role); return Response(result); } private ActionResult Response(IdentityResult result) { if (result.Succeeded) { return Ok(result); } foreach (var error in result.Errors) ModelState.AddModelError(error.Code, error.Description); return ValidationProblem(ModelState); }
В контроллере мы объявили четыре действия – создание, чтение, обновление и удаление ролей пользователей, а также один вспомогательный метод Response()
, который анализирует ответ ASP.NET Core Identity и формирует ответ сервера на запрос пользователя. В этих действиях мы используем методы сервиса RoleManager<TRole>
для получения роли по её идентификатору, а также методы создания, обновления и удаления ролей.
Чтобы протестировать работу этих действий, добавим в http-файл проекта следующие запросы
GET {{WebApplication6_HostAddress}}/roles/ ### POST {{WebApplication6_HostAddress}}/roles/ Content-Type: application/json { "Name": "User" } ### @id = ""; PUT {{WebApplication6_HostAddress}}/roles/{{id}} Content-Type: application/json { "Name": "Client" } ### DELETE {{WebApplication6_HostAddress}}/roles/{{id}}
Чтобы запрос PUT выполнялся успешно, необходимо подставить в переменную id
идентификатор обновляемой записи, который можно получить, выполнив запрос GET на получение всего списка ролей. Запустим приложение и проверим работу нового контроллера.
Результат выполнения запроса GET
Результат выполнения запроса POST на добавление новой роли
Результат выполнения запросов PUT и DELETE на обновление роли и удаление роли будет аналогичен результатам выполнения запроса POST – сервер вернет либо объект IdentityResult
, либо ошибку.
Теперь, когда мы можем управлять ролями пользователей в приложении, мы можем также использовать эти роли в приложении и выстраивать более сложную логику работы приложения. Однако, при этом, мы уже не сможем использовать новые роли в свойствах атрибута [Authorize]
, а проверять наличие роли пользователя фактически вручную, используя методы сервиса UserManager<TUser>
.
Итого
Для включения механизмов авторизации в приложении ASP.NET Core Web API используется специальный атрибут для [Authorize]
, применяемый к действиям контроллера или классу контроллера полностью. Авторизация пользователей по ролям — один из популярных способов авторизации, при котором один пользователь может принадлежать к одной или нескольким ролям и, в зависимости от этого, производится его авторизация в системе.