ASP.NET 5使用两个或多个策略进行授权(OR组合策略)

81

在ASP.NET 5,rc1中,是否可能根据两个或多个策略进行授权?

[Authorize(Policy = "Limited,Full")]
public class FooBarController : Controller
{
    // This code doesn't work
}

如果不使用策略,我该如何实现这一点?有两组用户可以访问此控制器:“完整”和“有限”。用户可能属于“完整”或“有限”,也可能同时属于两个组。他们只需要属于其中一个组即可访问此控制器。
6个回答

67

不是你想要的方式;策略被设计为累积的。例如,如果您使用两个单独的属性,则它们必须都通过。

您必须在单个策略中评估OR条件。但是您不必将其编码为单个处理程序中的OR。您可以有一个具有多个处理程序的要求。如果任一处理程序标记成功,则已满足该要求。请参见我的授权工作坊中的第6步。


1
如果策略是累加的,为什么在使用自定义策略时会替换默认值?这个问题的核心来自于这个问题。我正在声明自定义策略,并且不希望未经身份验证的请求进入我的授权处理程序。我目前使用的方法是从您的授权工作坊中的第2步(授权所有端点并在需要的地方放置[AllowAnonymous])。感觉像是反模式,但我可能很蠢! - steamrolla
1
基本上我们假设如果您正在设置自己的策略,则知道自己在做什么。应用策略表示您将覆盖默认设置。 - blowdart
明白了。只是感觉默认策略应该作为“基线”,就像是你在一系列自定义策略中的第一个策略一样。 - steamrolla
1
是的,这不太像是默认值,更像是“如果没有指定任何内容,则执行此操作。” - blowdart
@steamrolla,它们是累积的,但是ASP.NET授权使用最小权限方法来处理安全性,所有这些都必须通过,在您的情况下,[AllowAnonymous]已经通过,但可能会被以下策略阻止。 - Diego Mendes
如何创建一个带有AND条件的策略? - Christoph B

38

7

4
options.AddPolicy("ElevatedRights", policy => policy.RequireRole("Administrator", "PowerUser", "BackupAdministrator"));这段代码的意思是:添加一个名为"ElevatedRights"的策略,该策略要求用户具备"Administrator"、"PowerUser"或"BackupAdministrator"角色中的至少一个才能访问。 - CmdrTallen

5

对于我来说,使用按需动态创建的要求解决方案是最好的:

  1. 创建单独的“有限”和“完整”策略要求接口:
    public interface ILimitedRequirement : IAuthorizationRequirement { }
    public interface IFullRequirement : IAuthorizationRequirement { }
  1. 创建用于授权的自定义属性:
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
    public class AuthorizeAnyAttribute : AuthorizeAttribute {

        public string[] Policies { get; }

        public AuthorizeAnyAttribute(params string[] policies) : base(String.Join("Or", policies))
            => Policies = policies;
    }
  1. ILimitedRequirementIFullRequirement创建授权处理程序(请注意,这些处理程序处理的是接口,而不是类):
    public class LimitedRequirementHandler : AuthorizationHandler<ILimitedRequirement> {

        protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, ILimitedRequirement requirement) {
            if(limited){
                context.Succeed(requirement);
            }
        }
    }

    public class FullRequirementHandler : AuthorizationHandler<IFullRequirement> {

        protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, IFullRequirement requirement) {
            if(full){
                context.Succeed(requirement);
            }
        }
    }
  1. 如果您的授权处理程序很重(例如,其中一个访问数据库),并且您不希望另一个处理程序在另一个已经成功或失败的情况下进行授权检查,则可以使用以下解决方法(请记住,处理程序注册的顺序直接确定它们在请求管道中的执行顺序):
    public static class AuthorizationExtensions {

        public static bool IsAlreadyDetermined<TRequirement>(this AuthorizationHandlerContext context)
            where TRequirement : IAuthorizationRequirement
            => context.HasFailed || context.HasSucceeded
                || !context.PendingRequirements.Any(x => x is TRequirement);

    }


    public class LimitedRequirementHandler : AuthorizationHandler<ILimitedRequirement> {

        protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, ILimitedRequirement requirement) {
            if(context.IsAlreadyDetermined<ILimitedRequirement>())
                return;

            if(limited){
                context.Succeed(requirement);
            }
        }
    }

    public class FullRequirementHandler : AuthorizationHandler<IFullRequirement> {

        protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, IFullRequirement requirement) {
            if(context.IsAlreadyDetermined<IFullRequirement>())
                return;

            if(full){
                context.Succeed(requirement);
            }
        }
    }
  1. 注册授权处理程序(注意没有“LimiterOrFullRequirementHandler”,这两个处理程序将处理组合策略要求):
    //Order of handlers is important - it determines their execution order in request pipeline
    services.AddScoped<IAuthorizationHandler, LimitedRequirementHandler>();
    services.AddScoped<IAuthorizationHandler, FullRequirementHandler>();

现在我们需要检索所有的AuthorizeAny属性,并使用ImpromptuInterface(或任何其他用于动态创建类型实例的工具)为它们动态地创建要求。
    using ImpromptuInterface;

    List<AuthorizeAnyAttribute> attributes  = new List<AuthorizeAnyAttribute>();

    foreach(Type type in Assembly.GetExecutingAssembly().GetTypes().Where(type => type.IsAssignableTo(typeof(ControllerBase)))) {
        attributes.AddRange(Attribute.GetCustomAttributes(type , typeof(AuthorizeAnyAttribute))
            .Cast<AuthorizeAnyAttribute>()
            .Where(x => x.Policy != null));
        foreach(var methodInfo in type.GetMethods()) {
            attributes.AddRange(Attribute.GetCustomAttributes(methodInfo , typeof(AuthorizeAnyAttribute))
            .Cast<AuthorizeAnyAttribute>()
            .Where(x => x.Policy != null));
        }
    }
    
    //Add base requirement interface from which all requirements will be created on demand
    Dictionary<string, Type> baseRequirementTypes = new();
    baseRequirementTypes.Add("Limited", typeof(ILimitedRequirement));
    baseRequirementTypes.Add("Full", typeof(IFullRequirement));
    
    Dictionary<string, IAuthorizationRequirement> requirements = new();
    
    foreach(var attribute in attributes) {
        if(!requirements.ContainsKey(attribute.Policy)) {
            Type[] requirementTypes = new Type[attribute.Policies.Length];
            for(int i = 0; i < attribute.Policies.Length; i++) {
                if(!baseRequirementTypes.TryGetValue(attribute.Policies[i], out Type requirementType))
                    throw new ArgumentException($"Requirement for {attribute.Policies[i]} policy doesn't exist");
                requirementTypes[i] = requirementType;
            }
            //Creating instance of combined requirement dynamically
            IAuthorizationRequirement newRequirement = new { }.ActLike(requirementTypes);
            requirements.Add(attribute.Policy, newRequirement);
        }
    }
  1. 注册所有已创建的需求
    services.AddAuthorization(options => {
        foreach(KeyValuePair<string, IAuthorizationRequirement> item in requirements) {
             options.AddPolicy(item.Key, x => x.AddRequirements(item.Value));
        }
    }


以上解决方案允许处理单个要求,就像OR组合一样,如果默认的AuthorizeAttribute与自定义的AuthorizeAnyAttribute相同,则处理方式相同。
如果以上解决方案过于复杂,总是可以使用手动组合类型创建和注册:
1. 创建组合的“有限或完整”策略要求:
    public class LimitedOrFullRequirement : ILimitedRequirement, IFullRequirement { }

如果这两个要求除了使用组合的“有限或完整”策略外,还必须分别使用,那么针对单个需求创建接口实现:
    public class LimitedRequirement : ILimitedRequirement { }
    public class FullRequirement : IFullRequirement { }
  1. 注册策略(请注意,注释掉的策略完全是 可选的):
    services.AddAuthorization(options => {
                options.AddPolicy("Limited Or Full",
                    policy => policy.AddRequirements(new LimitedOrFullRequirement()));
                //If these policies also have single use, they need to be registered as well
                //options.AddPolicy("Limited",
                //  policy => policy.AddRequirements(new LimitedRequirement()));
                //options.AddPolicy("Full",
                //  policy => policy.AddRequirements(new FullRequirement()));
            });

4
我使用策略和角色:
[Authorize(Policy = "ManagerRights", Roles = "Administrator")]

如果您需要除内置角色以外的其他自定义角色,则需要高级订阅。 - Mark Homer
@MarkHomer 如果我没记错的话,在Windows上使用ASP.NET Core,角色也是声明,这应该可以工作。你所说的“高级订阅”是指有一个AD域可用吗? - undefined

0

假设你已经在使用

[Authorize(Policy = "Policy_A")]
public class ...

并且

[Authorize(Policy = "Policy_B")]
public class ...

使用

builder.Services.AddAuthorization(options =>
  options.AddPolicy("Policy_A", policyBuilder => 
    policyBuilder.RequireClaim("UserType", "A")));

builder.Services.AddAuthorization(options =>
  options.AddPolicy("Policy_B", policyBuilder =>
    policyBuilder.RequireClaim("UserType", "B")));

然后对于您的新PageModel或Controller,请使用

[Authorize(Policy = "Policy_A_or_B")]
public class ...

使用

builder.Services.AddAuthorization(options =>
  options.AddPolicy("Policy_A_or_B", policyBuilder =>
    policyBuilder.RequireClaim("UserType", "A", "B")));

还可以查看https://learn.microsoft.com/en-us/aspnet/core/security/authorization/policies


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接