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