Содержание
Политика авторизации — это набор критериев, включая роли, которым должен соответствовать пользователь, чтобы получить доступ к ресурсу.
Политики авторизации
Авторизация по ролям – не единственный способ разделения полномочий пользователей в приложении. Часто бывает необходимо, чтобы пользователь соответствовал не одному критерию (имел определенную роль), а сразу нескольким. Например, для доступа к определенному действию может потребоваться, чтобы:
- имел определенную роль;
- работал в определенном отделе;
- имел определенный стаж работы;
- и так далее
Очевидно, что использование для организации таких проверок исключительно ролей не подойдет, а проверка всех факторов может потребовать больших затрат времени. Для таких целей наиболее рационально использовать политики авторизации. Как мы уже знаем, использование объектов 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 появилось новое поле.
Теперь у нас всё готово для дальнейшей работы над созданием собственного ограничения. Для создания нового ограничения мы должны выполнить следующие условия:
- Разработать класс, выступающий в приложении ограничением. Этот класс должен реализовывать интерфейс
IAuthorizationRequirement - Необходимо создать обработчик авторизации, который будет наследовать абстрактный класс
AuthorizationHandler<TRequirement>и реализовывать его методHandleRequirementAsync() - Зарегистрировать обработчик авторизации, как сервис и добавить ограничение в список
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 политики авторизации и добавили в список созданное нами ограничение. Таким образом, теперь пользователь для получения доступа к защищенному ресурсу должен соответствовать трем ограничениям:
- иметь роль
Employee - иметь имя пользователя
Test - иметь стаж работы не менее трех лет
Чтобы мы могли воспользоваться новым ограничением, нам необходимо также обеспечить передачу сведений о дате начала работы сотрудника в список 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 мы можем создавать собственные политики авторизации, настраивать их с использованием уже готовых или же собственных ограничений авторизации.

