将 AuthorizationPolicy 绑定到 Controller/Action 而不使用 AuthorizeAttribute

Gor*_*aBC 5 c# authorization authorize-attribute asp.net-core

我想向我的 .NET Core API 添加授权。假设我有一个具有以下操作的 PersonController:

  • GetPerson(根据 id 检索 Person)

  • PostPerson(添加新人员)

  • DeletePerson(删除一个人)

     [Route("[controller]")]
     [ApiController]
     public class PersonController : ControllerBase
     {
         [HttpGet("{id}")]
         public async Task<ActionResult<PersonModel>> GetPerson(int id)
         {
             //
         }
    
         [HttpPost]
         public async Task<ActionResult<PersonModel>> PostPerson(PersonModel model)
         {
            //
         }
    
         [HttpDelete("{id}")]
         public async Task<ActionResult> DeletePerson(int id)
         {
            //
         }
     }
    
    Run Code Online (Sandbox Code Playgroud)

对于这个例子,我将使用两个角色。“ SuperAdmin ”应该能够执行所有操作,“ PersonReader ”应该只能执行 GetPerson 调用。尝试将 PostPerson 或 DeletePerson 作为 PersonReader 应该会失败。

我创建了以下授权策略:

                options.AddPolicy("SuperAdmin", policy =>
                    policy.RequireAuthenticatedUser()
                    .RequireRole("SuperAdmin")
                );
                options.AddPolicy("PersonReader", policy =>
                    policy.RequireAuthenticatedUser()
                    .RequireRole("PersonReader")
                );
Run Code Online (Sandbox Code Playgroud)

但现在我想将这些策略绑定到控制器操作,说明需要哪些策略才能执行控制器操作。我知道这可以通过这样的authorizationAttribute来完成:[Authorize(Policy="X"]但我希望能够在不使用AuthorizationAttributes的情况下做到这一点。

为什么我无法使用[授权]属性?
我不会讲太多细节,但是控制器的源代码是生成的。这意味着一旦再次生成,所有手动更改都将被覆盖。因此,授权不应该在控制器中。

startup.cs中,我将控制器映射到端点,如下所示:

app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
Run Code Online (Sandbox Code Playgroud)

可以为所有控制器绑定一个策略,如下所示:

endpoints.MapControllers().RequireAuthorization("SuperAdmin");
Run Code Online (Sandbox Code Playgroud)

但这意味着我将需要所有控制器操作的“超级管理员”策略。这样我就无法为特定操作定义所需的策略。我希望做这样的事情:

// pseudo-code  
// endpoints.MapControllerAction("GetPerson").RequireAuthorization("SuperAdmin", "PersonReader");
Run Code Online (Sandbox Code Playgroud)

不幸的是我找不到任何方法来做到这一点。有没有办法在不使用 [Authorize] 属性的情况下将策略绑定到控制器操作?

Kin*_*ing 5

您可以AuthorizeAttribute通过应用程序模型约定以编程方式应用或任何其他类型的属性IApplicationModelConvention。在那里您可以访问ApplicationModel包含所有已加载控制器的根目录,并且可以AuthorizeAttribute在那里添加控制器。每个控制器都由一个名为 的类表示ControllerModel。它实现了IFilterModel公开IFilterMetadata. 该模型还实现了ICommonModel公开属性列表,但该列表是只读的。因此,要修改该列表,您可能必须创建一个新模型来覆盖旧模型,这是相当复杂的。每个动作都由 表示,ActionModel也实现IFilterModel。因此,在这种情况下,我们不会尝试AuthorizeAttribute通过将其添加到属性列表来应用 ,而是将其转换为 an ,AuthorizeFilter它也是 an ,IFilterMetadata以便可以将其添加到 公开的过滤器列表中IFilterModel

这是详细代码:

public class AuthorizeAttributeInjectingConvention : IApplicationModelConvention
{
    readonly string _controller;
    readonly string _action;
    readonly AuthorizeFilter[] _authorizeFilters;
    public AuthorizeAttributeInjectingConvention(string controllerName, params AuthorizeAttribute[] authorizeAttributes) 
        : this(controllerName, null, authorizeAttributes)
    {                  
    }
    public AuthorizeAttributeInjectingConvention(string controllerName, string actionName, params AuthorizeAttribute[] authorizeAttributes)
    {
        _controller = controllerName;
        _action = actionName;
        _authorizeFilters = authorizeAttributes.Select(e => new AuthorizeFilter(new[] { e })).ToArray();
    }

    public void Apply(ApplicationModel application)
    {
        var filterModels = application.Controllers
                                     .Where(e => string.Equals(e.ControllerName, _controller, StringComparison.OrdinalIgnoreCase))
                                     .ToList<IFilterModel>();
        if(filterModels.Count > 0 && !string.IsNullOrWhiteSpace(_action))
        {
            filterModels = filterModels.Cast<ControllerModel>()
                                       .SelectMany(e => e.Actions.Where(o => string.Equals(o.ActionName, _action, StringComparison.OrdinalIgnoreCase)))
                                       .ToList<IFilterModel>();
        }
        foreach(var filterModel in filterModels)
        {                
            foreach(var af in _authorizeFilters)
            {
                filterModel.Filters.Add(af);
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

要注册IApplicationModelConvention,您可以将一个实例添加到通过MvcOptions. 为了方便起见,我创建了一组如下的扩展方法:

public static class AuthorizeAttributeInjectionMvcOptionsExtensions
{
    public static MvcOptions ApplyAuthorizeAttributes(this MvcOptions options, string controllerName, params AuthorizeAttribute[] authorizeAttributes)
    {
        return options.ApplyAuthorizeAttributes(controllerName, null, authorizeAttributes);
    }
    public static MvcOptions ApplyAuthorizeAttributes(this MvcOptions options, string controllerName, string actionName, params AuthorizeAttribute[] authorizeAttributes)
    {
        options.Conventions.Add(new AuthorizeAttributeInjectingConvention(controllerName, actionName, authorizeAttributes));
        return options;
    }
    public static MvcOptions ApplyAuthorizationPolicy(this MvcOptions options, string controllerName, string actionName, params string[] policies)
    {
        return options.ApplyAuthorizeAttributes(controllerName, actionName, policies.Select(e => new AuthorizeAttribute(e)).ToArray());
    }        
}
Run Code Online (Sandbox Code Playgroud)

现在,在 中Startup.ConfigureServices,您可以将AuthorizeAttribute您选择的 应用于特定控制器或操作(通过其名称),如下所示:

services.AddMvc(o => {
    //...

    //by AuthorizeAttribute
    var withSuperAdminAttr = new AuthorizeAttribute("SuperAdmin");
    o.ApplyAuthorizeAttributes("your_controller", "your_action", withSuperAdminAttr);

    //by policy
    o.ApplyAuthorizationPolicy("your_controller", "your_action", "SuperAdmin");
    //...
});
Run Code Online (Sandbox Code Playgroud)

请注意,上面的代码并不完美,它介绍了如何基本上实现它。您可以进一步改进的逻辑是如何过滤目标控制器和操作。在我的示例中,它只是根据控制器名称和操作名称进行过滤。我认为只要您有唯一的控制器名称和唯一的操作名称,这在几乎所有情况下都应该有效。否则,在实际应用AuthorizeAttribute.