簡體   English   中英

如何使用基於ASP.NET核心資源的授權,而無需在任何地方復制if / else代碼

[英]How to use ASP.NET Core resource-based authorization without duplicating if/else code everywhere

我有一個dotnet核心2.2 api,其中包含一些控制器和操作方法,需要根據用戶聲明和所訪問的資源進行授權。 基本上,每個用戶可以為每個資源擁有0個或多個“角色”。 這都是使用ASP.NET Identity Claims完成的。

所以,我的理解是我需要使用基於資源的授權 但是這兩個例子大多是相同的,並且需要在每個動作方法上使用顯式命令if / else邏輯,這正是我想要避免的。

我希望能夠做類似的事情

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

   return Json(resource);
}

而在其他地方將授權邏輯定義為策略/過濾器/要求/等等,並且可以訪問當前用戶聲明和端點接收的resourceId參數。 因此,我可以看到用戶是否有聲明表示他具有該特定resourceId的“管理員”角色。

您可以創建自己的屬性來檢查用戶的角色。 我在我的一個應用程序中完成了這個:

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")]

或更好的enum方法:

[RoleValidator(RoleType.Admin)]

或者您可以傳遞多個角色:

[RoleValidator(RoleType.User, RoleType.Admin)]

使用此解決方案,您還必須使用標准的Authorize屬性。

編輯:根據反饋使其動態化

RBAC和.NET中的主張的關鍵是創建您的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);
    }
}

為了解決這個問題,只需在啟動類中使用針對IApplicationBuilder的UseMiddleware擴展方法即可

app.UseMiddleware<RBACExampleMiddleware>();

我創建了一個AuthorizationHandler,它將查找查詢參數“tenant”,並根據角色成功或失敗。

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?tenant = tenant1

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

  3. 否定: https:// localhost:44337 / api / values

這種方法的關鍵是我從未真正返回403.代碼設置了身份,然后讓框架處理結果。 這可確保身份驗證與授權分開。

〜干杯

根據評論編輯

根據我的理解,您希望訪問當前用戶(與其相關的所有信息),您要為控制器(或操作)指定的角色以及端點接收的參數。 尚未嘗試過web api,但對於asp.net核心MVC,您可以通過在基於策略的AuthorizationHandler中使用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.ResourceAuthorizationFilterContext來實現的,這樣我們就可以從中訪問RouteData 對於UserResourceRequirement ,我們可以將其留空。

public class UserResourceRequirement : IAuthorizationRequirement { }

至於IRoleResourceService ,它是一個普通的服務類,因此我們可以向它注入任何東西。 此服務可替代將角色與代碼中的操作配對,這樣我們就不需要在操作的屬性中指定它。 這樣,我們可以自由選擇實現,例如:從數據庫,從配置文件或硬編碼。

通過注入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”策略。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM