我有一个dotnet core 2.2 API,其中一些控制器和操作方法需要根据用户索赔和访问资源进行授权。基本上,每个用户可以为每个资源具有0或许多"角色"。这一切都是使用ASP.NET身份声明完成的。
因此,我的理解是我需要利用基于资源的授权。但是,这两个示例大多都是相同的,并且如果/else logic在每个操作方法上,这都是我要避免的。
。我希望能够做
之类的事情[Authorize("Admin")] // or something similar
public async Task<IActionResult> GetSomething(int resourceId)
{
var resource = await SomeRepository.Get(resourceId);
return Json(resource);
}
和其他地方将授权逻辑定义为策略/过滤器/要求/任何内容,并且可以访问当前用户索赔和端点接收到的resourceId
参数。因此,我可以看到用户是否有声称表示他在该特定的resourceId
中具有" admin"角色。
编辑:基于反馈以使其动态
RBAC和.NET中的主张的关键是创建您的索赔身份,然后让框架完成工作。以下是一个示例中间件,将查看查询参数"用户"。然后基于字典生成索赔。
为了避免实际连接到身份提供商,我创建了一个设置索赔的中间件:
// **THIS CLASS IS ONLY TO DEMONSTRATE HOW THE ROLES NEED TO BE SETUP **
public class CreateFakeIdentityMiddleware
{
private readonly RequestDelegate _next;
public CreateFakeIdentityMiddleware(RequestDelegate next)
{
_next = next;
}
private readonly Dictionary<string, string[]> _tenantRoles = new Dictionary<string, string[]>
{
["tenant1"] = new string[] { "Admin", "Reader" },
["tenant2"] = new string[] { "Reader" },
};
public async Task InvokeAsync(HttpContext context)
{
// Assume this is the roles
List<Claim> claims = new List<Claim>
{
new Claim(ClaimTypes.Name, "John"),
new Claim(ClaimTypes.Email, "john@someemail.com")
};
foreach (KeyValuePair<string, string[]> tenantRole in _tenantRoles)
{
claims.AddRange(tenantRole.Value.Select(x => new Claim(ClaimTypes.Role, $"{tenantRole.Key}:{x}".ToLower())));
}
// Note: You need these for the AuthorizeAttribute.Roles
claims.AddRange(_tenantRoles.SelectMany(x => x.Value)
.Select(x => new Claim(ClaimTypes.Role, x.ToLower())));
context.User = new System.Security.Claims.ClaimsPrincipal(new ClaimsIdentity(claims,
"Bearer"));
await _next(context);
}
}
要将其汇总,只需在您的启动类中使用usemiddleware扩展方法进行iApplicationBuilder。
app.UseMiddleware<RBACExampleMiddleware>();
我创建了一个授权汉格勒,该授权将寻找"租户"查询参数。并根据角色成功或失败。
public class SetTenantIdentityHandler : AuthorizationHandler<TenantRoleRequirement>
{
public const string TENANT_KEY_QUERY_NAME = "tenant";
private static readonly ConcurrentDictionary<string, string[]> _methodRoles = new ConcurrentDictionary<string, string[]>();
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TenantRoleRequirement requirement)
{
if (HasRoleInTenant(context))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
private bool HasRoleInTenant(AuthorizationHandlerContext context)
{
if (context.Resource is AuthorizationFilterContext authorizationFilterContext)
{
if (authorizationFilterContext.HttpContext
.Request
.Query
.TryGetValue(TENANT_KEY_QUERY_NAME, out StringValues tenant)
&& !string.IsNullOrWhiteSpace(tenant))
{
if (TryGetRoles(authorizationFilterContext, tenant.ToString().ToLower(), out string[] roles))
{
if (context.User.HasClaim(x => roles.Any(r => x.Value == r)))
{
return true;
}
}
}
}
return false;
}
private bool TryGetRoles(AuthorizationFilterContext authorizationFilterContext,
string tenantId,
out string[] roles)
{
string actionId = authorizationFilterContext.ActionDescriptor.Id;
roles = null;
if (!_methodRoles.TryGetValue(actionId, out roles))
{
roles = authorizationFilterContext.Filters
.Where(x => x.GetType() == typeof(AuthorizeFilter))
.Select(x => x as AuthorizeFilter)
.Where(x => x != null)
.Select(x => x.Policy)
.SelectMany(x => x.Requirements)
.Where(x => x.GetType() == typeof(RolesAuthorizationRequirement))
.Select(x => x as RolesAuthorizationRequirement)
.SelectMany(x => x.AllowedRoles)
.ToArray();
_methodRoles.TryAdd(actionId, roles);
}
roles = roles?.Select(x => $"{tenantId}:{x}".ToLower())
.ToArray();
return roles != null;
}
}
tenantrolerequirement是一个非常简单的类:
public class TenantRoleRequirement : IAuthorizationRequirement { }
然后,您将所有内容连接到startup.cs文件中:
services.AddTransient<IAuthorizationHandler, SetTenantIdentityHandler>();
// Although this isn't used to generate the identity, it is needed
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Audience = "https://localhost:5000/";
options.Authority = "https://localhost:5000/identity/";
});
services.AddAuthorization(authConfig =>
{
authConfig.AddPolicy(Policies.HasRoleInTenant, policyBuilder => {
policyBuilder.RequireAuthenticatedUser();
policyBuilder.AddRequirements(new TenantRoleRequirement());
});
});
该方法看起来像这样:
// TOOD: Move roles to a constants/globals
[Authorize(Policy = Policies.HasRoleInTenant, Roles = "admin")]
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2" };
}
以下是测试方案:
正面:https://localhost:44337/api/values?tentant = tenant1
负面:https://localhost:44337/api/values?tentant = tenant2
负面:https://localhost:44337/api/values
这种方法的关键是我从未真正返回403。代码设置身份,然后让框架处理结果。这确保身份验证与授权分开。
您可以创建自己的属性,该属性将检查用户的角色。我已经在我的一个应用程序中完成了此操作:
public sealed class RoleValidator : Attribute, IAuthorizationFilter
{
private readonly IEnumerable<string> _roles;
public RoleValidator(params string[] roles) => _roles = roles;
public RoleValidator(string role) => _roles = new List<string> { role };
public void OnAuthorization(AuthorizationFilterContext filterContext)
{
if (filterContext.HttpContext.User.Claims == null || filterContext.HttpContext.User.Claims?.Count() <= 0)
{
filterContext.Result = new UnauthorizedResult();
return;
}
if (CheckUserRoles(filterContext.HttpContext.User.Claims))
return;
filterContext.Result = new ForbidResult();
}
private bool CheckUserRoles(IEnumerable<Claim> claims) =>
JsonConvert.DeserializeObject<List<RoleDto>>(claims.FirstOrDefault(x => x.Type.Equals(ClaimType.Roles.ToString()))?.Value)
.Any(x => _roles.Contains(x.Name));
}
它从索赔中获得用户角色,然后检查用户具有适当的角色来获得此重新启动。您可以这样使用:
[RoleValidator("Admin")]
或枚举的更好方法:
[RoleValidator(RoleType.Admin)]
,也可以传递多个角色:
[RoleValidator(RoleType.User, RoleType.Admin)]
使用此解决方案,您还必须使用标准授权属性。
根据注释进行编辑
根据我的理解,您要访问当前用户(与之相关的所有信息(,您要为控制器(或操作(指定的角色以及Endpoint收到的参数。尚未尝试使用Web API,但是对于ASP.NET Core MVC,您可以通过在基于策略的授权中使用AuthorizationHandler
来实现这一目标,并与专门为确定角色 - 资源访问的注入服务结合使用。
要这样做,首先在Startup.ConfigureServices
中设置策略:
services.AddAuthorization(options =>
{
options.AddPolicy("UserResource", policy => policy.Requirements.Add( new UserResourceRequirement() ));
});
services.AddScoped<IAuthorizationHandler, UserResourceHandler>();
services.AddScoped<IRoleResourceService, RoleResourceService>();
接下来创建UserResourceHandler
:
public class UserResourceHandler : AuthorizationHandler<UserResourceRequirement>
{
readonly IRoleResourceService _roleResourceService;
public UserResourceHandler (IRoleResourceService r)
{
_roleResourceService = r;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext authHandlerContext, UserResourceRequirement 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 (_roleResourceService.IsAuthorize(area, controller, action, id))
{
context.Succeed(requirement);
}
}
}
}
通过将context.Resource
施放到AuthorizationFilterContext
可以实现端点接收到的参数,以便我们可以从中访问RouteData
。至于UserResourceRequirement
,我们可以将其留为空。
public class UserResourceRequirement : IAuthorizationRequirement { }
至于IRoleResourceService
,这是一个普通的服务类,因此我们可以向其中注入任何东西。该服务是将角色与代码中的操作配对的替代品,因此我们不需要在Action的属性中指定它。这样,我们可以自由地选择实现,例如:从数据库,从配置文件或硬编码。
通过注入IHttpContextAccessor
来实现RoleResourceService
中访问用户。请注意,要使IHttpContextAccessor
注射可注射,请在Startup.ConfigurationServices
方法中添加services.AddHttpContextAccessor()
。
这是一个从配置文件获取信息的示例:
public class RoleResourceService : IRoleResourceService
{
readonly IConfiguration _config;
readonly IHttpContextAccessor _accessor;
readonly UserManager<AppUser> _userManager;
public class RoleResourceService(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 roleConfig = _config.GetValue<string>($"RoleSetting:{area}:{controller}:{action}"); //assuming we have the setting in appsettings.json
var appUser = await _userManager.GetUserAsync(_accessor.HttpContext.User);
var userRoles = await _userManager.GetRolesAsync(appUser);
// all of needed data are available now, do the logic of authorization
return result;
}
}
从数据库中获取设置肯定会更复杂,但是可以这样做,因为我们可以注入AppDbContext
。对于硬编码的方法,有很多方法可以做到。
完成后,请在操作上使用策略:
[Authorize(Policy = "UserResource")] //dont need Role name because of the RoleResourceService
public ActionResult<IActionResult> GetSomething(int resourceId)
{
//existing code
}
实际上,我们可以使用" USERRESOURCE"策略进行我们要应用的任何行动。
如果使用身份,则可以使用角色。只需致电授权并在资源或整个控制器上提供角色名称,甚至在授权中添加更多角色:
:[Authorize(Roles ="Clerk")]
我正在授权用店员名称的用户角色在某个资源上。要添加更多角色,只需在店员之后添加逗号,然后添加其他角色名称