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

Авторизация, основанная на пользовательской политике, позволяет вам создать богатую, многократно используемую и легко тестируемую структуру для авторизации.

Политика авторизации состоит из одного или нескольких требований и регистрируется при запуске приложения как часть конфигурации сервиса авторизации, расположенной в `` 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(), передав требование в качестве параметра.

Handlers must be registered in the services collection during configuration, for example;

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.
}
Поделись хорошей новостью с друзьями!
Следи за новостями!