ASP.NET Core 中使用 IAuthorizationPolicyProvider 的自定义授权策略提供程序

作者:Mike Rousos

通常在使用基于策略的授权时,通过在授权服务配置中调用 AuthorizationOptions.AddPolicy 来注册策略。 在某些情况下,可能无法(或不可取)采用此方式注册所有授权策略。 在这些情况下,可以使用自定义 IAuthorizationPolicyProvider 来控制如何提供授权策略。

自定义 IAuthorizationPolicyProvider 可能十分有用的方案示例包括:

  • 使用外部服务提供策略评估。
  • 使用大范围的策略(例如对于不同房间号或年龄),因此使用 AuthorizationOptions.AddPolicy 调用添加每个单独授权策略没有意义。
  • 在运行时基于外部数据源(如数据库)中的信息创建策略,或通过其他机制动态确定授权要求。

AspNetCore GitHub 存储库查看或下载示例代码。 下载 dotnet/AspNetCore 存储库 ZIP 文件。 解压缩文件。 导航到 src/Security/samples/CustomPolicyProvider 项目文件夹。

自定义策略检索

ASP.NET Core 应用使用 IAuthorizationPolicyProvider 接口的实现检索授权策略。 默认情况下,会注册并使用 DefaultAuthorizationPolicyProviderDefaultAuthorizationPolicyProviderIServiceCollection.AddAuthorization 调用中提供的 AuthorizationOptions 返回策略。

可通过在应用的依赖项注入容器中注册不同 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()}";
        }
    }
}

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

可以采用与其他 Authorize 属性相同的方式将它应用于操作,只不过它采用整数作为参数。

[MinimumAgeAuthorize(10)]
public IActionResult RequiresMinimumAge10()

自定义 IAuthorizationPolicyProvider

通过自定义 MinimumAgeAuthorizeAttribute 可以轻松地为所需的任何最小年龄请求授权策略。 下一个要解决的问题是确保授权策略可用于所有这些不同的年龄。 这便是 IAuthorizationPolicyProvider 的用武之地。

使用 MinimumAgeAuthorizationAttribute 时,授权策略名称会遵循模式 "MinimumAge" + Age,因此自定义 IAuthorizationPolicyProvider 应通过以下方式生成授权策略:

  • 从策略名称分析年龄。
  • 使用 AuthorizationPolicyBuilder 创建新 AuthorizationPolicy
  • 在此示例和下面的示例中,假定用户通过 cookie 进行身份验证。 AuthorizationPolicyBuilder 应使用至少一个授权方案名称进行构造或是始终成功。 否则,不会提供任何有关如何向用户提供质询的信息,会引发异常。
  • 使用 AuthorizationPolicyBuilder.AddRequirements 基于年龄向策略添加要求。 在其他方案中,可以改为使用 RequireClaimRequireRoleRequireUserName
internal class MinimumAgePolicyProvider : IAuthorizationPolicyProvider
{
    const string POLICY_PREFIX = "MinimumAge";

    // Policies are looked up by string name, so expect 'parameters' (like age)
    // to be embedded in the policy names. This 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

private DefaultAuthorizationPolicyProvider BackupPolicyProvider { get; }

public MinimumAgePolicyProvider(IOptions<AuthorizationOptions> options)
{
    // ASP.NET Core only uses one authorization policy provider, so 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 类型。

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

    services.AddSingleton<IAuthorizationPolicyProvider, MinimumAgePolicyProvider>();
    

完整的自定义 IAuthorizationPolicyProvider 示例可在 dotnet/aspnetcore GitHub 存储库中获得。