Переопределить глобальный фильтр авторизации в ASP.NET Core 1.0 MVC

Я пытаюсь настроить авторизацию в веб-приложении ASP.NET Core 1.0 (MVC 6).

Более ограничительный подход. По умолчанию я хочу ограничить все контроллеры и методы действий пользователями с ролью Admin. Итак, я добавляю глобальный атрибут authorize, например:

AuthorizationPolicy requireAdminRole = new AuthorizationPolicyBuilder()
    .RequireAuthenticatedUser()
    .RequireRole("Admin")
    .Build();
services.AddMvc(options => { options.Filters.Add(new AuthorizeFilter(requireAdminRole));});

Затем я хочу разрешить пользователям с определенными ролями обращаться к конкретным контроллерам. Например:

[Authorize(Roles="Admin,UserManager")]
public class UserControler : Controller{}

Что, конечно, не будет работать, так как "глобальный фильтр" не позволит UserManager получить доступ к контроллеру, поскольку они не являются "админами".

В MVC5 я смог реализовать это, создав настраиваемый атрибут authorize и поместив туда свою логику. Затем используйте этот настраиваемый атрибут как глобальный. Например:

public class IsAdminOrAuthorizeAttribute : AuthorizeAttribute
{
    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        ActionDescriptor action = filterContext.ActionDescriptor;
        if (action.IsDefined(typeof(AuthorizeAttribute), true) ||
            action.ControllerDescriptor.IsDefined(typeof(AuthorizeAttribute), true))
        {
            return;
        }

        base.OnAuthorization(filterContext);
    }
}

Я попытался создать пользовательский AuthorizeFilter, но не успел. API кажется другим.

Итак, мой вопрос: возможно ли установить политику по умолчанию, а затем переопределить ее для определенных контроллеров и действий. Или что-то подобное. Я не хочу идти с этим

[Authorize(Roles="Admin,[OtherRoles]")]

на каждом контроллере/действии, поскольку это потенциальная проблема безопасности. Что произойдет, если я случайно забуду поставить роль Admin.

Ответы

Ответ 1

Вам нужно будет немного поиграть с картой, поскольку ваша глобальная политика более ограничительна, чем та, которую вы хотите применить к определенным контроллерам и действиям:

  • По умолчанию только пользователи Admin могут получить доступ к вашему приложению
  • Конкретным ролям также будет предоставлен доступ к некоторым контроллерам (например, к Менеджерам пользователей, обращающимся к UsersController)

Как вы уже отмечали, наличие глобального фильтра означает, что только администраторы получат доступ к контроллеру. Когда вы добавите дополнительный атрибут в UsersController, будут доступны только те пользователи, которые и Admin и UserManager.

Можно использовать аналогичный подход к MVC 5, но он работает по-другому.

  • В MVC 6 атрибут [Authorize] не содержит логики авторизации.
  • Вместо AuthorizeFilter используется метод OnAuthorizeAsync, вызывающий службу авторизации, чтобы убедиться, что политики выполнены.
  • Конкретный IApplicationModelProvider используется для добавления AuthorizeFilter для каждого контроллера и действия с атрибутом [Authorize].

Один из вариантов может состоять в том, чтобы воссоздать ваш IsAdminOrAuthorizeAttribute, но на этот раз как AuthorizeFilter, который вы затем добавите в качестве глобального фильтра:

public class IsAdminOrAuthorizeFilter : AuthorizeFilter
{
    public IsAdminOrAuthorizeFilter(AuthorizationPolicy policy): base(policy)
    {
    }

    public override Task OnAuthorizationAsync(Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext context)
    {
        // If there is another authorize filter, do nothing
        if (context.Filters.Any(item => item is IAsyncAuthorizationFilter && item != this))
        {
            return Task.FromResult(0);
        }

        //Otherwise apply this policy
        return base.OnAuthorizationAsync(context);
    }        
}

services.AddMvc(opts => 
{
    opts.Filters.Add(new IsAdminOrAuthorizeFilter(new AuthorizationPolicyBuilder().RequireRole("admin").Build()));
});

Это применит ваш глобальный фильтр только тогда, когда контроллер/действие не имеет определенного атрибута [Authorize].


Вы также можете избежать использования глобального фильтра, введя себя в процесс, который генерирует фильтры для каждого контроллера и действия. Вы можете добавить свой собственный IApplicationModelProvider или свой собственный IApplicationModelConvention. Оба позволяют добавлять/удалять определенные фильтры контроллеров и действий.

Например, вы можете определить политику авторизации по умолчанию и дополнительные конкретные политики:

services.AddAuthorization(opts =>
{
    opts.DefaultPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().RequireRole("admin").Build();
    opts.AddPolicy("Users", policy => policy.RequireAuthenticatedUser().RequireRole("admin", "users"));
});

Затем вы можете создать новый IApplicatioModelProvider, который добавит политику по умолчанию для каждого контроллера, у которого нет своего собственного атрибута [Authorize] (соглашение с приложением было бы очень похоже и, вероятно, более согласовано с тем, как структура предназначенный для расширения. Я просто использовал существующий AuthorizationApplicationModelProvider в качестве руководства):

public class OverridableDefaultAuthorizationApplicationModelProvider : IApplicationModelProvider
{
    private readonly AuthorizationOptions _authorizationOptions;

    public OverridableDefaultAuthorizationApplicationModelProvider(IOptions<AuthorizationOptions> authorizationOptionsAccessor)
    {
        _authorizationOptions = authorizationOptionsAccessor.Value;
    }

    public int Order
    {
        //It will be executed after AuthorizationApplicationModelProvider, which has order -990
        get { return 0; }
    }

    public void OnProvidersExecuted(ApplicationModelProviderContext context)
    {
        foreach (var controllerModel in context.Result.Controllers)
        {
            if (controllerModel.Filters.OfType<IAsyncAuthorizationFilter>().FirstOrDefault() == null)
            {
                //default policy only used when there is no authorize filter in the controller
                controllerModel.Filters.Add(new AuthorizeFilter(_authorizationOptions.DefaultPolicy));
            }
        }
    }

    public void OnProvidersExecuting(ApplicationModelProviderContext context)
    {            
        //empty    
    }
}

//Register in Startup.ConfigureServices
services.TryAddEnumerable(
    ServiceDescriptor.Transient<IApplicationModelProvider, OverridableDefaultAuthorizationApplicationModelProvider>());

При этом на этих двух контроллерах будет использоваться политика по умолчанию:

public class FooController : Controller

[Authorize]
public class BarController : Controller

И определенная политика пользователей будет использоваться здесь:

[Authorize(Policy = "Users")]
public class UsersController : Controller

Обратите внимание, что вам все равно нужно добавить роль администратора в каждую политику, но по крайней мере все ваши политики будут объявлены одним методом запуска. Вероятно, вы могли бы создать свои собственные методы для создания политик, которые всегда будут добавлять роль администратора.

Ответ 2

Используя решение @Daniel, я столкнулся с той же проблемой, о которой упоминал @TarkaDaal в комментарии (там 2 AuthorizeFilter в контексте для каждого вызова... не совсем уверен, откуда они идут).

Итак, мой способ решить эту проблему:

public class IsAdminOrAuthorizeFilter : AuthorizeFilter
{
    public IsAdminOrAuthorizeFilter(AuthorizationPolicy policy): base(policy)
    {
    }

    public override Task OnAuthorizationAsync(Microsoft.AspNet.Mvc.Filters.AuthorizationContext context)
    {
        if (context.Filters.Any(f =>
        {
            var filter = f as AuthorizeFilter;
            //There 2 default Authorize filter in the context for some reason...so we need to filter out the empty ones
            return filter?.AuthorizeData != null && filter.AuthorizeData.Any() && f != this;
        }))
        {
            return Task.FromResult(0);
        }

        //Otherwise apply this policy
        return base.OnAuthorizationAsync(context);
    }        
}

services.AddMvc(opts => 
{
    opts.Filters.Add(new IsAdminOrAuthorizeFilter(new AuthorizationPolicyBuilder().RequireRole("admin").Build()));
});

Это уродливо, но он работает в этом случае, потому что, если вы используете атрибут Authorize без аргументов, вы все равно будете обрабатываться фильтром new AuthorizationPolicyBuilder().RequireRole("admin").Build().