在 ASP.NET Core 中将自定义授权策略提供程序与 IAuthorizationPolicyProvider 配合使用

作者:Mike Rousos

在具有 基于策略的授权的典型实现中,通过调用 AuthorizationOptions.AddPolicy 作为授权服务配置的一部分注册策略。 有时,可能无法(或不适合)以这种方式注册所有授权策略。 在这些方案中,可以 实现自定义 IAuthorizationPolicyProvider 来控制授权策略的提供方式。

自定义 IAuthorizationPolicyProvider 可能会派上用场的场景示例包括:

  • 使用外部服务提供策略评估。

  • 支持广泛的策略(例如针对不同房间号或年龄的策略);在这种情况下,通过一次 AuthorizationOptions.AddPolicy 调用逐个添加每项授权策略并不合理。

  • 根据外部数据源(如数据库)中的信息在运行时创建策略,或者通过另一种机制动态确定授权要求。

本文介绍如何实现 IAuthorizationPolicyProvider 接口,以便为 ASP.NET Core应用使用自定义授权策略提供程序。

代码示例

AspNetCore GitHub 存储库查看或下载示例代码

下载 dotnet/AspNetCore 存储库 ZIP 文件,并解压缩该文件。 示例代码位于 src/Security/samples/CustomPolicyProvider 项目文件夹中。

自定义策略检索

ASP.NET 核心应用使用接口的 IAuthorizationPolicyProvider 实现来检索授权策略。 默认情况下,会注册并使用 DefaultAuthorizationPolicyProvider 类。 DefaultAuthorizationPolicyProvider 返回在调用 AuthorizationOptions 方法时提供的 IServiceCollection.AddAuthorization 中的策略。

通过在应用的依赖注入容器中注册不同的IAuthorizationPolicyProvider实现来定制此行为。

IAuthorizationPolicyProvider 接口包含三个 API:

  • 该方法 GetPolicyAsync 返回给定名称的授权策略。
  • 该方法 GetDefaultPolicyAsync 返回默认授权策略,这是用于 [Authorize] 未指定策略的属性的策略。
  • 该方法 GetFallbackPolicyAsync 返回回退授权策略,这是未指定任何策略时授权中间件使用的策略。

通过实现这些 API,可以自定义授权策略的提供方式。

参数化授权属性示例

一个 IAuthorizationPolicyProvider 有用的场景是启用自定义 [Authorize] 属性,而这些属性的要求取决于参数。 例如,在 基于策略的授权 文档中,基于年龄的 (“AtLeast21”) 策略用作示例。 如果应用中的不同控制器操作可供 不同 年龄的用户使用,则具有许多不同的基于年龄的策略可能很有用。 与其在AuthorizationOptions中注册应用可能需要的所有不同的基于年龄的策略,不如使用自定义IAuthorizationPolicyProvider动态生成这些策略。 若要更轻松地使用策略,可以使用自定义授权属性(例如 [MinimumAgeAuthorize(20)])对操作进行注释。

自定义授权属性

授权策略由其名称标识。 MinimumAgeAuthorizeAttribute 的自定义实现需要将参数映射为一个可用于检索相应授权策略的字符串。 若要完成此任务,可以从 AuthorizeAttribute 派生,并使 Age 属性包装 AuthorizeAttribute.Policy 属性。

internal class MinimumAgeAuthorizeAttribute : AuthorizeAttribute
{
    const string POLICY_PREFIX = "MinimumAge";

    public MinimumAgeAuthorizeAttribute(int age) => Age = age;

    // Get or set the Age property by manipulating the underlying Policy property
    public int Age
    {
        get
        {
            if (int.TryParse(Policy.Substring(POLICY_PREFIX.Length), out var age))
            {
                return age;
            }
            return default(int);
        }
        set
        {
            Policy = $"{POLICY_PREFIX}{value.ToString()}";
        }
    }
}

此属性类型的字符串是基于硬编码前缀 Policy"MinimumAge")和通过构造函数传入的整数构建的。

可以像其他 Authorize 属性一样将它应用于操作,只不过它采用整数作为参数。

[MinimumAgeAuthorize(10)]
public IActionResult RequiresMinimumAge10()

自定义 IAuthorizationPolicyProvider

通过自定义 MinimumAgeAuthorizeAttribute 实现,可以轻松为所需的任意最低年龄请求授权策略。 下一个问题是确保授权策略适用于所有年龄。 在开发的这个阶段,IAuthorizationPolicyProvider 很有用。

实现 MinimumAgeAuthorizeAttribute时,授权策略名称遵循模式 "MinimumAge" + Age,因此自定义 IAuthorizationPolicyProvider 应通过完成以下任务来生成授权策略:

  • 从策略名称中解析年龄。
  • 使用 AuthorizationPolicyBuilder 创建新 AuthorizationPolicy
  • 此示例和后续示例假定用户通过使用 cookie 完成身份验证。 因此, AuthorizationPolicyBuilder 应使用至少一个授权方案名称构造,或者始终成功。 否则,将没有关于如何向用户发出质询的信息,并且会抛出异常。
  • 使用 AuthorizationPolicyBuilder.AddRequirements 方法,根据年龄向策略中添加要求。 在其他场景中,你也可以改用RequireClaimRequireRoleRequireUserName等这些方法。
internal class MinimumAgePolicyProvider : IAuthorizationPolicyProvider
{
    const string POLICY_PREFIX = "MinimumAge";

    // Policy lookup is by string name. Expect 'parameters' (like age) to be embedded in policy names.
    // The process is abstracted away from developers by the more strongly-typed attributes
    // derived from AuthorizeAttribute (like [MinimumAgeAuthorize()] in this sample).
    public Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
    {
        if (policyName.StartsWith(POLICY_PREFIX, StringComparison.OrdinalIgnoreCase) &&
            int.TryParse(policyName.Substring(POLICY_PREFIX.Length), out var age))
        {
            var policy = new AuthorizationPolicyBuilder(CookieAuthenticationDefaults.AuthenticationScheme);
            policy.AddRequirements(new MinimumAgeRequirement(age));
            return Task.FromResult(policy.Build());
        }

        return Task.FromResult<AuthorizationPolicy>(null);
    }
}

多个授权策略提供程序

使用自定义 IAuthorizationPolicyProvider 实现时,请记住,ASP.NET Core 仅使用一个实例 IAuthorizationPolicyProvider。 如果自定义提供程序无法为将要使用的所有策略名称提供授权策略,则应交由后备提供程序处理。

例如,考虑需要自定义年龄策略和更传统的基于角色的策略检索的应用程序。 此类应用可以使用以下自定义授权策略提供程序:

  • 尝试分析策略名称。
  • 如果策略名称不包含年龄,则调用其他策略提供程序(例如 DefaultAuthorizationPolicyProvider)。

可以通过在其构造函数中创建备份策略提供程序,更新前面描述的 IAuthorizationPolicyProvider 实现以使用 DefaultAuthorizationPolicyProvider。 如果策略名称与其预期模式 "MinimumAge" + Age不匹配,则备份可用。

private DefaultAuthorizationPolicyProvider BackupPolicyProvider { get; }

public MinimumAgePolicyProvider(IOptions<AuthorizationOptions> options)
{
    // ASP.NET Core only uses one authorization policy provider.
    // If the custom implementation doesn't handle all policies,
    // it should fall back to an alternate provider.
    BackupPolicyProvider = new DefaultAuthorizationPolicyProvider(options);
}

然后,可以更新该方法 GetPolicyAsync 以使用 BackupPolicyProvider 而不是让它返回 null:

...
return BackupPolicyProvider.GetPolicyAsync(policyName);

默认策略

除了提供命名授权策略之外,自定义的IAuthorizationPolicyProvider 还需要实现GetDefaultPolicyAsync,以便为未指定策略名称的[Authorize]属性提供授权策略。

在许多情况下,此授权属性仅要求用户已通过身份验证,因此你可以通过调用 RequireAuthenticatedUser 方法来创建所需的策略:

public Task<AuthorizationPolicy> GetDefaultPolicyAsync() => 
    Task.FromResult(new AuthorizationPolicyBuilder(CookieAuthenticationDefaults.AuthenticationScheme).RequireAuthenticatedUser().Build());

与自定义 IAuthorizationPolicyProvider的所有方面一样,可以根据需要进行自定义。 在某些情况下,可能适合从后备 IAuthorizationPolicyProvider 获取默认策略。

回退策略

自定义 IAuthorizationPolicyProvider 可选择性地实现 GetFallbackPolicyAsync 方法,以提供 在组合策略 和未指定策略时要使用的策略。 GetFallbackPolicyAsync如果该方法返回非 null 策略,则当未为请求指定策略时,授权中间件将使用返回的策略。

如果不需要回退策略,提供程序可以返回 null 或交由回退提供程序处理。

public Task<AuthorizationPolicy> GetFallbackPolicyAsync() => 
    Task.FromResult<AuthorizationPolicy>(null);

使用自定义 IAuthorizationPolicyProvider

若要从 IAuthorizationPolicyProvider 使用自定义策略,您 必须

  • 适当地使用依赖注入注册AuthorizationHandler类型(在基于策略的授权中描述),就像所有基于策略的授权方案一样。

  • 在应用程序依赖项注入服务集合IAuthorizationPolicyProvider中注册自定义Startup.ConfigureServices类型,并替换默认策略提供程序。

    services.AddSingleton<IAuthorizationPolicyProvider, MinimumAgePolicyProvider>();