如何使用ASP.NET Core基于资源的授权,而无需在所有地方重复if / else代码

emz*_*ero 7 c# asp.net authorization asp.net-identity asp.net-core

我有一个dotnet core 2.2 api,其中包含一些控制器和操作方法,需要根据用户声明和所访问的资源进行授权。基本上,每个用户可以为每个资源使用0个或多个“角色”。全部使用ASP.NET身份声明完成。

因此,我的理解是我需要利用基于资源的授权。但是,两个示例几乎都相同,并且每个操作方法都需要显式的命令式if / else逻辑,这就是我要避免的方法。

我希望能够做类似的事情

[Authorize("Admin")] // or something similar
public async Task<IActionResult> GetSomething(int resourceId)
{
   var resource = await SomeRepository.Get(resourceId);

   return Json(resource);
}
Run Code Online (Sandbox Code Playgroud)

在其他地方,将授权逻辑定义为策略/过滤器/需求/其他,并可以访问当前用户声明和resourceId端点接收的参数。因此,我可以看到用户是否有一个声明,表明该用户对该特定用户具有“管理员”角色resourceId

Rog*_*ala 6

编辑:根据反馈使其动态

.NET 中 RBAC 和声明的关键是创建您的 ClaimsIdentity,然后让框架完成它的工作。下面是一个示例中间件,它将查看查询参数“user”,然后根据字典生成 ClaimsPrincipal。

为了避免实际连接到身份提供者的需要,我创建了一个用于设置 ClaimsPrincipal 的中间件:

// **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);
    }
}
Run Code Online (Sandbox Code Playgroud)

要连接它,只需在启动类中使用IApplicationBuilder的UseMiddleware扩展方法。

app.UseMiddleware<RBACExampleMiddleware>();
Run Code Online (Sandbox Code Playgroud)

我创建一个 AuthorizationHandler ,它将查找查询参数“租户”,并根据角色成功或失败。

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

TenantRoleRequirement 是一个非常简单的类:

public class TenantRoleRequirement : IAuthorizationRequirement { }
Run Code Online (Sandbox Code Playgroud)

然后,将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());
    });
});
Run Code Online (Sandbox Code Playgroud)

该方法如下所示:

// TOOD: Move roles to a constants/globals
[Authorize(Policy = Policies.HasRoleInTenant, Roles = "admin")]
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
    return new string[] { "value1", "value2" };
}
Run Code Online (Sandbox Code Playgroud)

以下是测试场景:

  1. 正: https://localhost:44337/api/values?tenant=tenant1

  2. 负: https://localhost:44337/api/values?tenant=tenant2

  3. 负数:https://localhost:44337/api/values

这种方法的关键是我实际上从未返回 403。代码设置身份,然后让框架处理结果。这确保身份验证与授权分开。


Ben*_*kal 3

您可以创建自己的属性来检查用户的角色。我在我的一个应用程序中做到了这一点:

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));
}
Run Code Online (Sandbox Code Playgroud)

它从声明中获取用户角色,并检查用户是否具有获取此资源的适当角色。你可以这样使用它:

[RoleValidator("Admin")]
Run Code Online (Sandbox Code Playgroud)

或使用枚举的更好方法:

[RoleValidator(RoleType.Admin)]
Run Code Online (Sandbox Code Playgroud)

或者您可以传递多个角色:

[RoleValidator(RoleType.User, RoleType.Admin)]
Run Code Online (Sandbox Code Playgroud)

对于此解决方案,您还必须使用标准授权属性。

  • @Rogala 你能详细说明一下吗?我认为在我的情况下我无法使用内置安全框架,因为我需要执行一些自定义授权逻辑,该逻辑取决于传递给属性的角色名称和尝试访问的资源。 (2认同)