我正在努力寻找一个好的解决方案来进行自定义授权检查,而不必一遍又一遍地手动重复授权检查。
为了说明这一点,假设我对 .net core Web api 有以下设置,它有两个端点,一个用于 GET,一个用于 POST。我想检查(也许针对数据库)用户是否有权查看资源,或有权创建资源。
这就是文档中所说的基于资源的授权 看起来像这样:
[Authorize]
[ApiVersion ("1.0")]
[Route ("api/v{version:apiVersion}/[controller]")]
[ApiController]
public class ResourcesController : ControllerBase {
private readonly IAuthorizationService _authorizationService;
//..constructor DI
[HttpGet ("{resourceId}")]
public ActionResult<Resource> Get (Guid resourceId) {
var authorizationCheck = await _authorizationService.AuthorizeAsync (User, resourceId, ServiceOperations.Read);
if (!authorizationCheck.Succeeded) {
return Forbid ();
}
return Ok (ResourceRep.Get (resourceId));
}
[HttpPost]
public ActionResult<Resource> Post ([FromBody] Resource resource) {
var authorizationCheck = await _authorizationService.AuthorizeAsync (User, null, ServiceOperations.Write);
if (!authorizationCheck.Succeeded) {
return Forbid ();
}
return Ok (ResourceRep.Create (resource));
}
}
现在想象一下
ServiceOperations
枚举有一个很长的受支持操作列表,或者有 100 个不同的端点,我将不得不在各处进行相同的检查,或者更糟糕的是,可能会忘记添加一个检查,而我绝对应该添加一个检查查看。并且没有一种简单的方法可以在单元测试中找到它。
我想过使用属性,但正如文档所述:
属性评估发生在数据绑定之前以及执行加载文档的页面处理程序或操作之前。由于这些原因,使用 [Authorize] 属性的声明性授权是不够的。相反,您可以调用自定义授权方法 - 一种称为命令式授权的样式。
因此,当检查本身需要不可用的参数(resourceId)时,我似乎无法使用授权策略并用授权属性装饰方法(这很容易对它们的存在进行单元测试)。
所以对于问题本身: 一般如何使用命令式(基于资源的)授权而不必重复自己(这很容易出错)。我希望有一个如下所示的属性:
[HttpGet ("{resourceId}")]
[AuthorizeOperation(Operation = ServiceOperations.Read, Resource=resourceId)]
public ActionResult<Resource> Get (Guid resourceId) {..}
[AuthorizeOperation(Operation = ServiceOperations.Write)]
[HttpPost]
public ActionResult<Resource> Post ([FromBody] Resource resource) {..}
您可以在基于策略的授权中使用
AuthorizationHandler
来实现它,并与专门创建的注入服务相结合以确定操作资源配对。
为此,首先在
Startup.ConfigureServices
中设置策略:
services.AddAuthorization(options =>
{
options.AddPolicy("OperationResource", policy => policy.Requirements.Add( new OperationResourceRequirement() ));
});
services.AddScoped<IAuthorizationHandler, UserResourceHandler>();
services.AddScoped<IOperationResourceService, OperationResourceService>();
接下来创建
OperationResourceHandler
:
public class OperationResourceHandler: AuthorizationHandler<OperationResourceRequirement>
{
readonly IOperationResourceService _operationResourceService;
public OperationResourceHandler(IOperationResourceService o)
{
_operationResourceService = o;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext authHandlerContext, OperationResourceRequirement requirement)
{
if (context.Resource is AuthorizationFilterContext filterContext)
{
var area = (filterContext.RouteData.Values["area"] as string)?.ToLower();
var controller = (filterContext.RouteData.Values["controller"] as string)?.ToLower();
var action = (filterContext.RouteData.Values["action"] as string)?.ToLower();
var id = (filterContext.RouteData.Values["id"] as string)?.ToLower();
if (_operationResourceService.IsAuthorize(area, controller, action, id))
{
context.Succeed(requirement);
}
}
}
}
OperationResourceRequirement
可以是一个空类:
public class OperationResourceRequirement : IAuthorizationRequirement { }
技巧是,我们不是在属性中指定操作的操作,而是在其他地方指定它,例如在数据库中、在 appsettings.json 中、在某些配置文件中或硬编码中。
这是从配置文件中获取操作资源对的示例:
public class OperationResourceService : IOperationResourceService
{
readonly IConfiguration _config;
readonly IHttpContextAccessor _accessor;
readonly UserManager<AppUser> _userManager;
public class OpeartionResourceService(IConfiguration c, IHttpContextAccessor a, UserManager<AppUser> u)
{
_config = c;
_accessor = a;
_userManager = u;
}
public bool IsAuthorize(string area, string controller, string action, string id)
{
var operationConfig = _config.GetValue<string>($"OperationSetting:{area}:{controller}:{action}"); //assuming we have the setting in appsettings.json
var appUser = await _userManager.GetUserAsync(_accessor.HttpContext.User);
//all of needed data are available now, do the logic of authorization
return result;
}
}
请注意,要使
IHttpContextAccessor
可注入,请在 services.AddHttpContextAccessor()
方法主体中添加 Startup.ConfigurationServices
。
完成所有操作后,在操作上使用策略:
[HttpGet ("{resourceId}")]
[Authorize(Policy = "OperationResource")]
public ActionResult<Resource> Get (Guid resourceId) {..}
每个动作的授权策略可以相同。
我正在开发 ASP.NET 6.0 Web API 项目,遇到了授权范围和 API 版本控制问题。
我有一个具有多个 API 版本和共享的控制器 授权政策。
[Authorize(Policy = "AuthorizationScope")] [Route("endpoint", Name = "tmp")] [HttpPost, MapToApiVersion("1.0")] [HttpPost, MapToApiVersion("2.0")] public async Task<Response> FAsync([FromBody] RequestModel request) {}
在我的 Startup.cs 中,我设置了如下基本授权策略:
services.AddAuthorization(options => { options.AddPolicy("AuthorizationScope", builder => { builder.RequireScope("v1-Scope", "v2-Scope"); }); });
我遇到的问题是API版本不正确 按范围分开。例如:
具有 v1-Scope 的用户可以访问端点的版本 2,反之亦然。
由于两个范围都应用于两个版本,因此用户可能会意外访问他们不应该访问的端点。
我需要一种方法来执行以下政策:
v1-Scope 应该只能访问 API 的版本 1。
v2-Scope 应该只能访问 API 的版本 2。
我使用了一种更定制的方法,具有特定于版本的范围 映射,效果很好。
我在startup.cs文件中创建了一个自定义授权处理程序和要求
services.AddSingleton<IAuthorizationHandler, CombinedAuthorizationHandler>();
services.AddAuthorization(options =>
{
options.AddPolicy("AuthorizationScope", policy =>
{
var scopeVersionMapping = new Dictionary<string, string[]>
{
{ "v1-Scope", new[] { "1" } },
{ "v2-Scope", new[] { "2" } }
};
policy.Requirements.Add(new CombinedAuthorizationRequirement(scopeVersionMapping));
});
});
组合授权要求:我创建了一个映射的字典 范围(v1-Scope、v2-Scope)到特定 API 版本(1、2)。这确保了 根据 API 版本验证正确的范围 已请求。
CombinedAuthorizationHandler:此处理程序检查用户的范围并 验证它们是否与 API 版本匹配。如果范围有效 给定版本,请求被授权;否则,失败。
CombinedAuthorizationRequirement.cs
public class CombinedAuthorizationRequirement : IAuthorizationRequirement
{
public Dictionary<string, string[]> ScopeVersionMapping { get; }
public CombinedAuthorizationRequirement(Dictionary<string, string[]> scopeVersionMapping)
{
ScopeVersionMapping = scopeVersionMapping ?? throw new ArgumentNullException(nameof(scopeVersionMapping));
}
}
CombinedAuthorizationHandler.cs
public class CombinedAuthorizationHandler : AuthorizationHandler<CombinedAuthorizationRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CombinedAuthorizationRequirement requirement)
{
var userScopes = context.User.Claims
.Where(c => c.Type == "scope")
.Select(c => c.Value)
.ToList();
var hasValidScope = userScopes.Any(scope => requirement.ScopeVersionMapping.ContainsKey(scope) &&
requirement.ScopeVersionMapping[scope].Contains(GetApiVersion(context)));
if (hasValidScope)
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
return Task.CompletedTask;
}
private string GetApiVersion(AuthorizationHandlerContext context)
{
var httpContext = context.Resource as HttpContext;
if (httpContext == null)
{
return null;
}
var routeVersion = httpContext.Request.RouteValues["version"] as string;
return routeVersion;
}
}
使用此解决方案:
- v1-Scope 只能访问 version1/response_Of_V1 API。
- v2-Scope 只能访问 version2/response_Of_V2 API。
- 如果将来引入新的API版本或范围,我们只需要更新字典并调整控制器中的逻辑即可 根据需要。
该解决方案非常适合管理多个 API 版本 使用 1 个控制器实现不同的授权范围和多个响应。如果您正在与类似的人一起工作 您需要将特定范围映射到不同 API 的情况 版本,这种方法应该可以有效地解决问题。