覆盖 ASP.NET Core 3 中的授权策略

S. *_*nke 5 .net c# asp.net-core

编辑:这已经解决了。请在下面查看我的编辑。对于不需要“完全忽略声明”覆盖的更“默认”解决方案,请参阅已接受的答案。在撰写本文时,下面的 My EDIT 包含的代码可帮助您支持另一种情况,您希望完全忽略该要求而不是覆盖它。

我看过很多关于这个问题的帖子,但没有一个能真正解决我的问题。 可悲的是,一种有前途的方法对我不起作用。

我有一些政策。默认的要求存在声明。但另一项政策要求此声明不存在。如果默认的一个应用于控制器,我不能在方法上应用另一个。不是覆盖我以前的保单,而是将保单全部收集在一起,第一个保单失败,因为索赔不可用。

一个很好的例子:

启动:

services.AddAuthorization(options =>
{
    // 99% of the methods require you to have this claim, so we set this as default.
    options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
        .RequireAuthenticatedUser()
        .RequireClaim("UserId")
        .Build();

    options.AddPolicy("UnregisteredUsers",
        new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
            .RequireAuthenticatedUser() // You DO need to be authorized, but that does not mean you already have an account!
            .RequireAssertion(x => x.User.FindUserIdClaim() == null) // If you do not have a user ID, you can not access endpoints that require you to be registered until you have an ID
unregistered.
            .Build());

    // A more specific policy
    options.AddPolicy("Administrator",
        new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
            .RequireAuthenticatedUser()
            .RequireRole(Roles.Administrator)
            .Build());
});
Run Code Online (Sandbox Code Playgroud)

控制器:

[Route("api/[controller]")]
[ApiController]
[Authorize] // You could also use MapControllers().RequireAuthorization() in Startup.cs instead of [Authorize]
public class BaseController : ControllerBase
{

}

public class UsersController : BaseController
{

    // You can only access this endpoint IF you do not have an account.
    [HttpPost]
    [Authorize(Policy = "UnregisteredUsers")]
    public async Task<IActionResult> CreateAccount()
    {
       // Code here
       return Ok();
    }
    

    // This one uses the default policy, just like many other policies
    [HttpGet]
    public async Task<IActionResult> GetUsers()
    {
       // This would throw if unregistered users would access this endpoint because the claim is not set.
       var userId = User.GetUserIdClaim();
      
       // Other code here
       return Ok();      
    }
}
Run Code Online (Sandbox Code Playgroud)

如您所见,如果您尝试创建一个帐户,basecontroller 已经拒绝您访问,因为您没有该声明。但我要求声明不存在,所以这不是一个有用的评论。基本上,我的第二个政策比第一个更重要。

我希望你们能帮帮我!


编辑:

感谢@King King,我已经能够解决这个问题。他们的回答有效,但我需要对某些特定场景进行一些更改:

  • 订单显然不能保证
  • 当您想通过“扭转”来“覆盖”现有政策时,该答案有效。在这种情况下,默认情况下我需要一个用户 ID,但另一个策略要求您没有它。不支持开箱即用的情况是,当您的策略不关心您是否拥有用户 ID 时。我会在这里发布我的完整解决方案,但我非常感谢@King King 的帮助!
// Note: Scope is REQUIRED!
services.AddScoped<IAuthorizationHandler, UserIdClaimRequirementHandler>();

services.AddAuthorization(options =>
{
    // 99% of the methods require you to have this claim, so we set this as default.
    options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
        .RequireAuthenticatedUser()
        .AddRequirements(new UserIdClaimRequirement(UserIdClaimSetting.ClaimMustExist))
        .Build();

    options.AddPolicy("UnregisteredUsers",
        new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
            .RequireAuthenticatedUser() // You DO need to be authorized, but that does not mean you already have an account!
            .AddRequirements(new UserIdClaimRequirement(UserIdClaimSetting.ClaimMustNotExist)) // If you do not have a user ID, it means you are not in the database yet, which means you are unregistered.
            .Build());

    // Registed users and Unregistered users can access these endpoints, but they DO need to be authenticated.
    options.AddPolicy("AllUsers",
        new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
            .RequireAuthenticatedUser()
            .AddRequirements(new UserIdClaimRequirement(UserIdClaimSetting.IgnoreClaim))      
            .Build());

    // A more specific policy. This builds upon the default policy, so a user ID is required. Technically we could also omit the RequireAuthenticatedUser() call but I like that this is explicit.
    options.AddPolicy("Administrator",
        new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
            .RequireAuthenticatedUser()
            .RequireRole(Roles.Administrator)
            .Build());
});
Run Code Online (Sandbox Code Playgroud)

要求等级:

/// <summary>
/// Can be used to configure the requirement of the existence of the UserId claim.
/// Either it must exist, it must NOT exist or it does not matter. <br/><br/>
/// 
/// Please note: Order is not guaranteed with policies. <br/>
/// In the case when you use a <see cref="UserIdClaimSetting.IgnoreClaim"/> or <see cref="UserIdClaimSetting.ClaimMustNotExist"/> and the default policy uses a <see cref="UserIdClaimSetting.ClaimMustExist"/>,
/// the handler should prioritize the result of the <see cref="UserIdClaimSetting.IgnoreClaim"/> and <see cref="UserIdClaimSetting.ClaimMustNotExist"/> 
/// and just not evaluate <see cref="UserIdClaimSetting.ClaimMustExist"/> to prevent it returning 403 when that one runs last.
/// In order to do so, the handler MUST be registered as Scoped so it resets for the next reset.
/// </summary>
/// <remarks>
/// This is quite a silly solution! 
/// The reason it is necessary is because policies are built on top of each other.
/// The default policy requires that the claim exists because this is true for 99% of the requests, 
/// so it makes sense to make this the default to prevent having to explicitly setup authorization on each endpoint. <br/><br/>
/// The "UnregisteredUsers" policy requires that it does NOT exist.<br/><br/>
/// The "AllUsers" policy does not care if it exists or not. 
/// Your first thought is probably that using this requirement would be unnecessary in that case,
/// but if this requirement is not used there, the default policy's requirement will require the existence of the claim which will break this policy.
/// </remarks>
public class UserIdClaimRequirement : IAuthorizationRequirement
{
    public UserIdClaimSetting Setting { get; }

    public UserIdClaimRequirement(UserIdClaimSetting setting)
    {
        Setting = setting;
    }
}

public enum UserIdClaimSetting
{
    /// <summary>
    /// If the claim does <b>not</b> exist, authorization will fail
    /// </summary>
    ClaimMustExist,
    /// <summary>
    /// If the claim exists, authorization will fail
    /// </summary>
    ClaimMustNotExist,
    /// <summary>
    /// It does not matter if the claim exists. Either way, authorization will succeed.
    /// </summary>
    IgnoreClaim
}
Run Code Online (Sandbox Code Playgroud)

需求处理程序:

public class UserIdClaimRequirementHandler : AuthorizationHandler<UserIdClaimRequirement>
{
    /// <summary>
    /// Order is not guaranteed with policies. 
    /// In the case when you use a <see cref="UserIdClaimSetting.IgnoreClaim"/> or <see cref="UserIdClaimSetting.ClaimMustNotExist"/> and the default policy uses a <see cref="UserIdClaimSetting.ClaimMustExist"/>,
    /// the handler should prioritize the result of the <see cref="UserIdClaimSetting.IgnoreClaim"/> and <see cref="UserIdClaimSetting.ClaimMustNotExist"/> 
    /// and just not evaluate <see cref="UserIdClaimSetting.ClaimMustExist"/> to prevent it returning 403 when that one runs last.
    /// In order to do so, the handler MUST be registered as Scoped so it resets for the next reset.
    /// </summary>
    private bool _policyHasAlreadySucceeded = false;

    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, UserIdClaimRequirement requirement)
    {
        if(_policyHasAlreadySucceeded)
        {
            return Task.CompletedTask;
        }

        var hasUserId = context.User.FindFirst("UserId") != null;
        // If the claim must not exist but it does  -> FAIL
        // If the claim must exist but it does not  -> FAIL
        // If it doesn't matter if the claim exists -> SUCCEED
        if ((requirement.Setting == UserIdClaimSetting.ClaimMustNotExist && hasUserId) ||
            (requirement.Setting == UserIdClaimSetting.ClaimMustExist && !hasUserId) ||
            (requirement.Setting == UserIdClaimSetting.IgnoreClaim && false))
        {
            context.Fail();
        }
        else
        {
            // This requirement has succeeded!

            _policyHasAlreadySucceeded = requirement.Setting == UserIdClaimSetting.IgnoreClaim || requirement.Setting == UserIdClaimSetting.ClaimMustNotExist;

            // Also, if there are other policy requirements that use the UserId claim, just set them to SUCCEEDED because this requirement is more important than those.
            // Example: The default policy requires you to have a user id claim, while this requirement might be used by requiring the claim to NOT exist.
            // In order to make this work, we have to override the "require user id claim" requirement by telling it that it succeeded even though it did not!
            var otherUserIdClaimRequirements = context.Requirements.Where(e => e is UserIdClaimRequirement || e is ClaimsAuthorizationRequirement cu && cu.ClaimType == "UserId");
            foreach (var r in otherUserIdClaimRequirements)
            {
                context.Succeed(r);
            }
        }
        return Task.CompletedTask;
    }
}
Run Code Online (Sandbox Code Playgroud)

Kin*_*ing 10

授权要求处理程序是 AND 运算。因此,如果任何一个失败,那么整个都会失败。授权策略将转化为一组授权需求处理程序。根据我的调试,有 2 个授权要求从默认策略(在您的代码中)转换而来,即DenyAnonymousAuthorizationRequirement(对应于RequireAuthenticatedUser())和ClaimsAuthorizationRequirementwith ClaimType = "UserId"(对应于RequireClaim("UserId"))。

我发现自己有一种方法可以覆盖处理这两个要求的处理程序的结果(或者简单地跳过,我对此不太确定)。也就是说,通过实现一个自定义需求处理程序,您可以在其中AuthorizationHandlerContext访问公开需要处理的所有授​​权需求(当然包括我上面提到的两个)。通过调用Succeed它们,它们似乎被忽略了(再次处理或简单地跳过)。我们可以添加第二个自定义授权要求处理程序来验证这一点,但这根本不重要(所以我没有这样做)。

UnregisteredUsers以下是如何使用自定义授权要求处理程序而不是基于以下内容来构建自定义策略 ( ) RequireAssertion

//the custom requirement class which must implement IAuthorizationRequirement
public class NoUserIdClaimRequirement : IAuthorizationRequirement
{
}
//the corresponding handler
public class NoUserIdClaimRequirementHandler : AuthorizationHandler<NoUserIdClaimRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, NoUserIdClaimRequirement requirement)
    {
        var hasUserId = context.User.FindFirst("UserId") != null;
        if (hasUserId)
        {
            context.Fail();                
        } else
        {
            //NOTE: if you're sure about the extremely high priority of this requirement 
            //(so we can discard/ignore all the other requirements), just remove the .Where for a shorter code
            foreach (var r in context.Requirements
                                     .Where(e => e is NoUserIdClaimRequirement ||
                                                 e is ClaimsAuthorizationRequirement cu && cu.ClaimType == "UserId"))
            {
                //mark all as succeeded
                context.Succeed(r); 
            }
        }
        return Task.CompletedTask;
    }
}
Run Code Online (Sandbox Code Playgroud)

在上面的代码中,我知道用户仍然需要进行身份验证,因此我们不会Succeed调用DenyAnonymousAuthorizationRequirement. 如果情况并非如此,您可以将其包含在上面的过滤器中。

您需要在以下位置注册授权要求处理程序类型ConfigureServices

services.AddSingleton<IAuthorizationHandler, NoUserIdClaimRequirementHandler>();
Run Code Online (Sandbox Code Playgroud)

RequireAssertion现在,您需要像这样构建自定义策略,而不是使用:

options.AddPolicy("UnregisteredUsers",
                  x => x.RequireAuthenticatedUser()
                        .AddRequirements(new NoUserIdClaimRequirement()));
Run Code Online (Sandbox Code Playgroud)

我自己尝试了一个简单的演示,效果很好。但它可能需要您自己进行一些调整。如果发生任何错误,请告诉我。这里的代码只是为了展示解决此问题的可能方法的想法。您可以在此基础上构建更复杂和通用的解决方案。