Авторизация, основанная на пользовательской политике¶
Авторизация, основанная на пользовательской политике, позволяет вам создать богатую, многократно используемую и легко тестируемую структуру для авторизации.
Политика авторизации состоит из одного или нескольких требований и регистрируется при запуске приложения как часть конфигурации сервиса авторизации, расположенной в `` ConfigureServices()`` файла startup.cs
.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAuthorization(options =>
{
options.AddPolicy("Over21",
policy => policy.Requirements.Add(new MinimumAgeRequirement(21)));
}
});
Здесь вы можете увидеть политику “Over21” с одним требованием, которое касается минимального возрастного ограничения, и она передается в качестве параметра.
Политики применяются с помощью атрибута Authorize
, где указывается имя политики, например:
[Authorize(Policy="Over21")]
public class AlcoholPurchaseRequirementsController : Controller
{
public ActionResult Login()
{
}
public ActionResult Logout()
{
}
}
Требования¶
Авторизационное требование - это коллекция параметров данных, которую может использовать политика для оценки текущего пользователя. В нашей политике MinimumAge у требования есть один параметр - минимальный возраст. Требование должно реализовывать IAuthorizationRequirement
. Это простой интерфейс. Требование выглядит следующим образом:
public class MinimumAgeRequirement : IAuthorizationRequirement
{
public MinimumAgeRequirement(int age)
{
MinimumAge = age;
}
protected int MinimumAge { get; set; }
}
У требования нет никаких данных или свойств.
Обработчики авторизации¶
Обработчик авторизации отвечает за оценку любых свойств требования и сравнивает их с AuthorizationContext, чтобы принять решение, разрешена ли авторизация. У требования может быть несколько обработчиков. Обработчики должны наследоваться от AuthorizationHandler<T>
, где T - это требование, которое обрабатывается.
public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
public override Task HandleRequirementAsync(AuthorizationHandlerContext context, MinimumAgeRequirement requirement)
{
if (!context.User.HasClaim(c => c.Type == ClaimTypes.DateOfBirth &&
c.Issuer == "http://contoso.com"))
{
return Task.FromResult(0);
}
var dateOfBirth = Convert.ToDateTime(context.User.FindFirst(
c => c.Type == ClaimTypes.DateOfBirth && c.Issuer == "http://contoso.com").Value);
int calculatedAge = DateTime.Today.Year - dateOfBirth.Year;
if (dateOfBirth > DateTime.Today.AddYears(-calculatedAge))
{
calculatedAge--;
}
if (calculatedAge >= requirement.MinimumAge)
{
context.Succeed(requirement);
}
}
}
Здесь мы сперва смотрим, предоставил ли текущий пользователь claim даты рождения. Если claim отсутствует, мы не можем авторизовать пользователя, так что мы возвращаемся. Если у нас есть claim, и мы выяснили, сколько пользователю лет, а также этот возраст соответствует минимальному возрасту требования, тогда авторизация будет успешной, так что мы вызываем context.Succeed()
, передав требование в качестве параметра.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAuthorization(options =>
{
options.AddPolicy("Over21",
policy => policy.Requirements.Add(new MinimumAgeRequirement(21)));
});
services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
}
Каждый обработчик добавляется к коллекции сервисов с помощью services.AddSingleton<IAuthorizationHandler, YourHandlerClass>();
, и это передается в класс обработчика.
Что должен возвращать обработчик?¶
В нашем примере обработчика метод Handle()
не возвращает значение, так как мы определим успех или неудачу обработки?
- Обработчик определяет успех, вызывая
context.Succeed(IAuthorizationRequirement requirement)
, передавая требование, которое было успешно валидировано. - Обработчик вообще не должен обрабатывать неудачные попытки, поскольку другие обработчики для этого требования могут быть успешными.
- Если вы хотите все же отобразить неудачу, даже если другие обработчики для требования успешны, то можете вызвать
context.Fail()
.
Независимо от того, что вы вызываете внутри обработчика, все обработчики для требования будут вызываться, если политике нужно требование. Тогда у требования могут быть побочные эффекты, например, логирование, которое всегда будет присутствовать, даже если был вызван context.Fail()
.
Зачем нужно несколько обработчиков для требования?¶
Если вы хотите, чтоб оценка проводилась на основе OR, то нужно реализовать несколько обработчиков для одного требования. Например, в офисах Microsoft двери открываются только с помощью карт-ключей или, если вы забыли ключ, вам откроют на ресепшене, и тогда вам выдается карта на один день. В данном случае мы имеем одно требование, EnterBuilding, но несколько обработчиков для этого требования.
public class EnterBuildingRequirement : IAuthorizationRequirement
{
}
public class BadgeEntryHandler : AuthorizationHandler<EnterBuildingRequirement>
{
protected override void Handle(AuthorizationContext context, EnterBuildingRequirement requirement)
{
if (context.User.HasClaim(c => c.Type == ClaimTypes.BadgeId &&
c.Issuer == "http://microsoftsecurity"))
{
context.Succeed(requirement);
}
}
}
public class HasTemporaryStickerHandler : AuthorizationHandler<EnterBuildingRequirement>
{
protected override void Handle(AuthorizationContext context, EnterBuildingRequirement requirement)
{
if (context.User.HasClaim(c => c.Type == ClaimTypes.TemporaryBadgeId &&
c.Issuer == "https://microsoftsecurity"))
{
// We'd also check the expiration date on the sticker.
context.Succeed(requirement);
}
}
}
Допустим, оба обработчика являются зарегистрированными, политика оценивает EnterBuildingRequirement, и если любой из обработчиков завершит работу успешно, оценка политики также пройдет успешно.
Использование функции для выполнения политики¶
Могут случаться ситуации, когда выполнение политики проще выразить в коде. Можно просто передать Func<AuthorizationHandlerContext, bool>
при настройке политики с помощью RequireAssertion
.
Например, предыдущий BadgeEntryHandler
можно переписать вот так:
services.AddAuthorization(options =>
{
options.AddPolicy("BadgeEntry",
policy => policy.RequireAssertion(context =>
context.User.HasClaim(c =>
(c.Type == ClaimTypes.BadgeId ||
c.Type == ClaimTypes.TemporaryBadgeId)
&& c.Issuer == "https://microsoftsecurity"));
}));
}
}
Доступ к контексту запросов MVC в обработчиках¶
У метода Handle
, который вы должны реализовать, есть два параметра, AuthorizationContext
и Requirement
. Такие фреймворки, как MVC или Jabbr, могут свободно добавлять любой объект в свойство Resource
для AuthorizationContext
, чтобы передавать дополнительную информацию.
Например, MVC передает экземпляр Microsoft.AspNet.Mvc.Filters.AuthorizationFilterContext
в свойство Resource, которое используется для доступа к HttpContext, RouteData и тд.
Использование свойства Resource
конкретно для определенного фреймворка. Использование информации в Resource
ограничит политику авторизации для конкретного фреймворка. Вы должны преобразовать свойство Resource
с помощью ключевого слова as
, а затем проверить, что преобразование прошло успешно, чтобы убедиться, что код не полетит с ошибкой InvalidCastExceptions
при запуске на других фреймворках:
var mvcContext = context.Resource as Microsoft.AspNet.Mvc.Filters.AuthorizationFilterContext;
if (mvcContext != null)
{
// Examine MVC specific things like routing data.
}