Содержание
На данный момент у нас в проекте имеется контроллер для работы с учётными записями пользователей и их аутентификации, а также контроллер 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], применяемый к действиям контроллера или классу контроллера полностью. Авторизация пользователей по ролям — один из популярных способов авторизации, при котором один пользователь может принадлежать к одной или нескольким ролям и, в зависимости от этого, производится его авторизация в системе.






