为了账号安全,请及时绑定邮箱和手机立即绑定

如何在不重复 if/else 代码的情况下使用 ASP.NET Core 基于资源的授权

如何在不重复 if/else 代码的情况下使用 ASP.NET Core 基于资源的授权

C#
HUWWW 2023-05-13 16:13:54
我有一个 dotnet core 2.2 api,其中包含一些控制器和操作方法,需要根据用户声明和正在访问的资源进行授权。基本上,每个用户可以对每个资源拥有 0 个或多个“角色”。这一切都是使用 ASP.NET Identity Claims 完成的。所以,我的理解是我需要使用Resource-based authorization。但是这两个例子大部分是相同的,并且在每个操作方法上都需要明确的命令式 if/else 逻辑,这是我试图避免的。我希望能够做类似的事情[Authorize("Admin")] // or something similarpublic async Task<IActionResult> GetSomething(int resourceId){   var resource = await SomeRepository.Get(resourceId);   return Json(resource);}在其他地方将授权逻辑定义为策略/过滤器/要求/任何东西,并可以访问当前用户声明和resourceId端点接收的参数。所以我可以看到用户是否有一个声明,表明他拥有该特定resourceId.
查看完整描述

3 回答

?
米脂

TA贡献1836条经验 获得超3个赞

编辑:根据反馈使其动态化


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,它将查找查询参数“租户”,并根据角色成功或失败。


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。代码设置身份,然后让框架处理结果。这确保身份验证与授权分开。


查看完整回答
反对 回复 2023-05-13
?
繁华开满天机

TA贡献1816条经验 获得超4个赞

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


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

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


查看完整回答
反对 回复 2023-05-13
?
德玛西亚99

TA贡献1770条经验 获得超3个赞

根据评论编辑


根据我的理解,您想访问当前用户(所有相关信息)、您要为控制器(或操作)指定的角色以及端点接收的参数。还没有尝试过 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,它是一个普通的服务类,因此我们可以向它注入任何东西。此服务是将角色与代码中的动作配对的替代品,因此我们无需在动作的属性中指定它。这样,我们就可以自由选择实现方式,例如:来自数据库、来自配置文件或硬编码。


访问用户RoleResourceService是通过注入实现的IHttpContextAccessor。请注意,要使IHttpContextAccessor可注射,请services.AddHttpContextAccessor()在Startup.ConfigurationServices方法体中添加。


这是从配置文件获取信息的示例:


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”策略。


查看完整回答
反对 回复 2023-05-13
  • 3 回答
  • 0 关注
  • 125 浏览

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信