不使用 AuthorizeAttribute 将 AuthorizationPolicy 绑定到 Controller/Action
Bind AuthorizationPolicy to Controller/Action without using AuthorizeAttribute
我想为我的 .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)
{
//
}
}
对于这个例子,我将使用两个角色。 'SuperAdmin' 应该能够执行所有操作,而 'PersonReader' 应该只能执行 GetPerson 调用。尝试将 PostPerson 或 DeletePerson 作为 PersonReader 应该会失败。
我创建了以下授权策略:
options.AddPolicy("SuperAdmin", policy =>
policy.RequireAuthenticatedUser()
.RequireRole("SuperAdmin")
);
options.AddPolicy("PersonReader", policy =>
policy.RequireAuthenticatedUser()
.RequireRole("PersonReader")
);
但是现在我想把这些策略绑定到controller action上,就是说需要什么样的policy才能做controller action。我知道这可以通过这样的 authorizationAttribute 来完成:[Authorize(Policy="X"]
但我希望能够在不使用 AuthorizationAttributes 的情况下做到这一点。
为什么我不能使用[授权]属性?
我就不多说了,但是Controller的源代码是生成的。这意味着一旦再次生成,所有手动更改都将被覆盖。因此授权不应该在控制器中。
在 startup.cs 中,我将控制器映射到这样的端点:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
可以像这样为所有控制器绑定一个策略:
endpoints.MapControllers().RequireAuthorization("SuperAdmin");
但这意味着我需要对所有控制器操作使用 'SuperAdmin' 策略。有了这个,我无法为特定操作定义所需的策略。我希望做这样的事情:
// pseudo-code
// endpoints.MapControllerAction("GetPerson").RequireAuthorization("SuperAdmin", "PersonReader");
不幸的是,我找不到任何方法来做到这一点。有没有办法在不使用 [Authorize] 属性的情况下将策略绑定到控制器操作?
您可以通过应用程序模型约定 IApplicationModelConvention
以编程方式应用 AuthorizeAttribute
或任何其他类型的属性。在那里你可以访问包含所有加载控制器的根 ApplicationModel
,你可以在那里添加 AuthorizeAttribute
。每个控制器由一个名为 ControllerModel
的 class 表示。它实现了 IFilterModel
,公开了 IFilterMetadata
的列表。该模型还实现了 ICommonModel
,它公开了一个属性列表,但是这个列表是只读的。因此,要修改该列表,您可能必须创建一个新模型来覆盖旧模型,这相当复杂。每个动作都由 ActionModel
表示,它也实现了 IFilterModel
。所以在这种情况下,我们不会尝试通过将 AuthorizeAttribute
添加到属性列表来应用它,而是将其转换为 AuthorizeFilter
,它也是一个 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);
}
}
}
}
要注册 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());
}
}
现在在 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");
//...
});
请注意,上面的代码并不完美,它介绍了您基本上可以如何实现它。您可以进一步改进的逻辑是如何过滤目标控制器和操作。在我的示例中,它只是根据控制器名称和操作名称进行过滤。我认为只要您具有唯一的控制器名称和唯一的操作名称,这在几乎所有情况下都应该有效。否则,在实际应用 AuthorizeAttribute
.
之前,您可能必须添加更多自定义逻辑来定位正确的控制器和操作
我想为我的 .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) { // } }
对于这个例子,我将使用两个角色。 'SuperAdmin' 应该能够执行所有操作,而 'PersonReader' 应该只能执行 GetPerson 调用。尝试将 PostPerson 或 DeletePerson 作为 PersonReader 应该会失败。
我创建了以下授权策略:
options.AddPolicy("SuperAdmin", policy =>
policy.RequireAuthenticatedUser()
.RequireRole("SuperAdmin")
);
options.AddPolicy("PersonReader", policy =>
policy.RequireAuthenticatedUser()
.RequireRole("PersonReader")
);
但是现在我想把这些策略绑定到controller action上,就是说需要什么样的policy才能做controller action。我知道这可以通过这样的 authorizationAttribute 来完成:[Authorize(Policy="X"]
但我希望能够在不使用 AuthorizationAttributes 的情况下做到这一点。
为什么我不能使用[授权]属性?
我就不多说了,但是Controller的源代码是生成的。这意味着一旦再次生成,所有手动更改都将被覆盖。因此授权不应该在控制器中。
在 startup.cs 中,我将控制器映射到这样的端点:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
可以像这样为所有控制器绑定一个策略:
endpoints.MapControllers().RequireAuthorization("SuperAdmin");
但这意味着我需要对所有控制器操作使用 'SuperAdmin' 策略。有了这个,我无法为特定操作定义所需的策略。我希望做这样的事情:
// pseudo-code
// endpoints.MapControllerAction("GetPerson").RequireAuthorization("SuperAdmin", "PersonReader");
不幸的是,我找不到任何方法来做到这一点。有没有办法在不使用 [Authorize] 属性的情况下将策略绑定到控制器操作?
您可以通过应用程序模型约定 IApplicationModelConvention
以编程方式应用 AuthorizeAttribute
或任何其他类型的属性。在那里你可以访问包含所有加载控制器的根 ApplicationModel
,你可以在那里添加 AuthorizeAttribute
。每个控制器由一个名为 ControllerModel
的 class 表示。它实现了 IFilterModel
,公开了 IFilterMetadata
的列表。该模型还实现了 ICommonModel
,它公开了一个属性列表,但是这个列表是只读的。因此,要修改该列表,您可能必须创建一个新模型来覆盖旧模型,这相当复杂。每个动作都由 ActionModel
表示,它也实现了 IFilterModel
。所以在这种情况下,我们不会尝试通过将 AuthorizeAttribute
添加到属性列表来应用它,而是将其转换为 AuthorizeFilter
,它也是一个 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);
}
}
}
}
要注册 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());
}
}
现在在 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");
//...
});
请注意,上面的代码并不完美,它介绍了您基本上可以如何实现它。您可以进一步改进的逻辑是如何过滤目标控制器和操作。在我的示例中,它只是根据控制器名称和操作名称进行过滤。我认为只要您具有唯一的控制器名称和唯一的操作名称,这在几乎所有情况下都应该有效。否则,在实际应用 AuthorizeAttribute
.