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

Политика авторизации — это набор критериев, включая роли, которым должен соответствовать пользователь, чтобы получить доступ к ресурсу.

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

Политики авторизации

Авторизация по ролям – не единственный способ разделения полномочий пользователей в приложении. Часто бывает необходимо, чтобы пользователь соответствовал не одному критерию (имел определенную роль), а сразу нескольким. Например, для доступа к определенному действию может потребоваться, чтобы:

  1. имел определенную роль;
  2. работал в определенном отделе;
  3. имел определенный стаж работы;
  4. и так далее

Очевидно, что использование для организации таких проверок исключительно ролей не подойдет, а проверка всех факторов может потребовать больших затрат времени. Для таких целей наиболее рационально использовать политики авторизации.  Как мы уже знаем, использование объектов Claim позволяет нам «зашить» в JWT-токен самую разнообразную информацию, которая может нам потребоваться для авторизации пользователя. Благодаря этому мы можем организовывать в приложении самые разнообразные схемы авторизации пользователей. Попробуем добавить политику авторизации в наше приложение.

Добавление новой политики авторизации

Политики авторизации настраиваются при добавлении сервисов авторизации в методе расширения AddAuthorization(). Например, добавим политику авторизации в нашем приложении. Откроем файл Program.cs и добавим вызов метода AddAuthorization() следующим образом:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("FirstPolicy",
    policy =>
    {
        policy.RequireRole("Employee");
        policy.RequireUserName("User");
    });
});

Здесь options – это делегат вида Action<AuthorizationOptions>. У класса AuthorizationOptions определены следующие важные для нас свойства и методы

Наименование Тип/возвращаемое значение Описание
Свойства
DefaultPolicy AuthorizationPolicy Политика авторизации, используемая по умолчанию
Методы
AddPolicy (string name, Action<AuthorizationPolicyBuilder> configurePolicy) void Добавляет новую политику с именем name и настройками configurePolicy
GetPolicy (string name); AuthorizationPolicy? Возвращает политику с именем name

Таким образом, в нашем примере мы добавляем в приложение новую политику авторизации с именем “FirstPolicy”, используя делегат Action<AuthorizationPolicyBuilder>.

В свою очередь, класс AuthorizationPolicyBuilder применяется для настройки новой политики и содержит следующие основные методы

Наименование Возвращаемое значение Описание
AddRequirements (params IAuthorizationRequirement[] requirements); AuthorizationPolicyBuilder Добавляет пользовательское условие авторизации в политику
RequireAuthenticatedUser (); AuthorizationPolicyBuilder Указывает, что пользователь должен быть аутентифицирован, чтобы соответствовать политике авторизации
RequireClaim (string claimType); AuthorizationPolicyBuilder Требует, чтобы у пользователя должен был определен Claim с именем claimType. При этом значение Claim может быть произвольным. Этот метод имеет ряд перегруженных версий, позволяющих указать значения Claim, которое должно быть указано у пользователя
RequireRole (params string[] roles); AuthorizationPolicyBuilder Требует, чтобы у пользователя была определена одна или несколько ролей из списка roles
RequireUserName (string userName); AuthorizationPolicyBuilder Требует, чтобы свойство UserName пользователя было равно userName

Кроме этих методов, AuthorizationPolicyBuilder содержит также методы для объединения различных политик авторизации. Итого, нашу политику с именем «FirstPolicy» можно прочитать следующим образом:

чтобы пользователь успешно прошел авторизацию его свойство UserName должно быть равным значению «User» и у пользователя должна быть определена роль «Employee».

Остается применить созданную политику авторизации в нашем приложении. Для применения политик безопасности, так же, как и при использовании авторизации по ролям используется атрибут [Authorize]. Например, применим нашу политику безопасности для действия Get() контроллера WeatherForecastController

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme, Policy = "FirstPolicy")]
public IEnumerable<WeatherForecast> Get()

Теперь попробуем выполнить запрос к этому действию. На данный момент у нас уже есть пользователь, принадлежащий к роли Employee. При этом, его имя не соответствует политике безопасности, поэтому, если попробовать получить доступ к действию Get(), то мы гарантировано получим ошибку:

Можете изменить политику безопасности следующим образом:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("FirstPolicy",
    policy =>
    {
        policy.RequireRole("Employee");
        policy.RequireUserName("Test");
    });
});

и убедиться, что теперь запрос выполняется успешно — авторизация пользователя с использованием политик безопасности производится успешно.

Создание собственных ограничений для политик авторизации

В целом, ASP.NET Core позволяет, что называется «из коробки» создавать самые различные по сложности и гибкости политики авторизации. Однако, иногда нам требуется обеспечить какой-то свой собственный функционал, для которого нет готовых методов ASP.NET Core. Например, мы хотим создать политику авторизации, в которой будет проверяться стаж работы сотрудника, и пользователь получал доступ к ресурсу только при наличии стажа более 5 лет. Для такого условия в ASP.NET Core нет готовых методов AuthorizationPolicyBuilder и, следовательно, нам необходимо создать собственное ограничение для политики авторизации.

Для начала, добавим необходимое свойство для класса User:

public class User : IdentityUser<int>
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime BeginningOfWork { get; set; }
}

Для свойств как стаж работы, возраст и т.д., которые зависят от текущей даты, не следует определять тип, соответствующий абсолютному значению величины, например, int. Выгоднее задавать такие значение как DateOnly или DateTime и высчитывать необходимое значение по мере необходимости. Поэтому в нашем классе мы определили свойство BeginningOfWork (начало работы) как DateTime.

Создадим новую миграцию, чтобы при следующем запросе контекста база данных обновилась

PM> Add-Migration AddBeginningOfWork

Запустим приложение SQLite Expert и убедимся, что миграция была применена и в таблице AspNetUsers появилось новое поле.

Теперь у нас всё готово для дальнейшей работы над созданием собственного ограничения. Для создания нового ограничения мы должны выполнить следующие условия:

  1. Разработать класс, выступающий в приложении ограничением. Этот класс должен реализовывать интерфейс IAuthorizationRequirement
  2. Необходимо создать обработчик авторизации, который будет наследовать абстрактный класс AuthorizationHandler<TRequirement> и реализовывать его метод HandleRequirementAsync()
  3. Зарегистрировать обработчик авторизации, как сервис и добавить ограничение в список Requirements политики авторизации.

Реализуем эти пункты по порядку. Создадим в проекте папку Requirements и добавим в ней класс ExperienceRequirement следующего содержания

public class ExperienceRequirement : IAuthorizationRequirement
{
    public int Experience { get; set; }

    public ExperienceRequirement(int experience)
    {
        Experience = experience;
    }
}

Как можно видеть, класс ExperienceRequirement не содержит никаких методов, а только хранит в своем единственном свойстве Experience ограничение (стаж работы сотрудника). Чтобы использовать это ограничение при обработке запроса пользователя нам необходимо реализовать обработчик авторизации, который будет использовать созданное ограничение. Для этого добавим в проект новый класс с названием ExperienceHandler. Класс можно размещать в любом удобном месте проекта, например, в той же папке Requirements. Сам код обработчика представлен ниже

public class ExperienceHandler : AuthorizationHandler<ExperienceRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ExperienceRequirement requirement)
    {
        var beginingOfWorkClaim = context.User.FindFirst("BeginningOfWork");
        if (beginingOfWorkClaim != null)
        {
            if (DateTime.TryParse(beginingOfWorkClaim.Value, out DateTime beginingOfWork))
            {
                DateTime now = DateTime.Today;
                int Experience = now.Year - beginingOfWork.Year;
                if (beginingOfWork > now.AddYears(-Experience))
                    Experience--;
                if (Experience >= requirement.Experience)
                    context.Succeed(requirement);
            }
        }
        return Task.CompletedTask;
    }
}

В этом классе мы переопределили метод HandleRequirementAsync(), который вызывается каждый раз, когда пользователь пытается получить доступ к защищенному ресурсу для которого применяется ограничение. В качестве параметров метода используется некий контекст обработчика AuthorizationHandlerContext, используя который мы можем получить сведения о пользователе, который выполняет запрос, список используемых ограничений для ресурса и так далее, а также непосредственно само ограничение, выполнение которого необходимо проверить внутри метода. В нашем случае таким ограничением выступает объект класса ExperienceRequirement.

Таким образом, всё, что мы делаем внутри метода – это вычисляем стаж работы сотрудника, используя для этого текущую дату и дату, которая должна быть указана в утверждении (объекте типа Claim) BeginningOfWork для пользователя, осуществляющего запрос к защищенному ресурсу. Если ограничение выполняется, то мы вызываем метод Succeed() контекста обработчика, сообщая тем самым, что ограничение выполнено. Так же мы можем, при необходимости, вызвать и противоположный метод Fail(), сообщающий, что ограничение не выполняется.

Следующий наш шаг – регистрация обработчика в качестве сервиса и указание нового ограничения для политики авторизации. Эту работу мы проведем в файле Program.cs.

builder.Services.AddTransient<IAuthorizationHandler, ExperienceHandler>();
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("FirstPolicy",
    policy =>
    {
        policy.RequireRole("Employee");
        policy.RequireUserName("Test");
        policy.Requirements.Add(new ExperienceRequirement(3));
    });
});

В первой строке мы зарегистрировали новый сервис в контейнере зависимостей. Далее мы воспользовались свойством Requirements политики авторизации и добавили в список созданное нами ограничение. Таким образом, теперь пользователь для получения доступа к защищенному ресурсу должен соответствовать трем ограничениям:

  1. иметь роль Employee
  2. иметь имя пользователя Test
  3. иметь стаж работы не менее трех лет

Чтобы мы могли воспользоваться новым ограничением, нам необходимо также обеспечить передачу сведений о дате начала работы сотрудника в список Claim, который мы, в дальнейшем, передаем в качестве полезной нагрузки в JWT-токен. Изменим метод GetUserClaims() контроллера UsersController следующим образом:

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));
    claims.Add(new("BeginningOfWork", _user.BeginningOfWork.ToString()));
    return claims;
}

Здесь в предпоследней строке метода мы добавляем утверждение с именем «BeginningOfWork» и значением, указанным при регистрации пользователя.

Теперь у нас всё готово для проверки работы приложения. Как и ранее, мы должны аутентифицировать пользователя и передать JWT-токен в запросе на получение прогноза погоды. Эти операции мы уже производили ни один раз, поэтому повторять их не имеет особого смысла. Можете самостоятельно попытаться получить доступ к защищенному ресурсу, а мы лучше перейдем к следующей части, касающейся авторизации и аутентификации пользователя.

Итого

Политика авторизации — это набор критериев, включая роли, которым должен соответствовать пользователь, чтобы получить доступ к ресурсу. В ASP.NET Core Web API мы можем создавать собственные политики авторизации, настраивать их с использованием уже готовых или же собственных ограничений авторизации.

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