如何使用ASP.NET基于资源的授权而无需复制/其他代码



我有一个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" };
}

以下是测试方案:

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

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

  3. 负面: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")]

我正在授权用店员名称的用户角色在某个资源上。要添加更多角色,只需在店员之后添加逗号,然后添加其他角色名称

相关内容

  • 没有找到相关文章