Авторизация пользователей по ролям

На данный момент у нас в проекте имеется контроллер для работы с учётными записями пользователей и их аутентификации, а также контроллер WeatherForecastController, который создается по умолчанию в шаблонном проекте ASP.NET Core Web API. Допустим, мы хотим позволить получать данные о прогнозе погоду только авторизованным пользователям. В ASP.NET Core сделать это достаточно просто, используя специальный атрибут [Authorize].

В этой статье используется приложение, которое разрабатывается на протяжении всей главы. Весь исходник приложения с последними изменениями находится по этой ссылке

Атрибут [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-файле проекта запрос на получение данных о прогнозе погоды (этот запрос также создается по умолчанию в шаблонном проекте). Если мы попытаемся выполнить этот запрос, то сервер ответит следующей ошибкой

Чтобы запрос был выполнен успешно нам необходимо:

  1. Выполнить аутентификацию пользователя и получить JWT-токен.
  2. Передать токен в заголовке запроса на добавление нового проекта.

Добавим в 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, который мы можем использовать в своих приложениях.

Чтобы добавить роли пользователей в приложении вы можете действовать двумя способами:

  1. Добавить необходимые роли, используя миграции EF Core. Такой вариант, в основном, предполагает, что все роли пользователей будут определены один раз – в момент применения миграции и пользователи не будут иметь возможность их каким-либо образом изменять. Такой способ является наиболее простым в реализации.
  2. Организовать работу с ролями, используя сервис 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 в котором определили роль. Если необходимо определить в этом свойстве несколько ролей, то они перечисляются через запятую. Теперь только те пользователи, которые имеют одну или несколько из перечисленных ролей, смогут получить доступ к действию.

Проверка авторизации по ролям

Чтобы проверить авторизацию по ролям, нам необходимо:

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

Подписаться
Уведомить о
guest
0 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии