ASP.NET Core 中基于策略的授权

在底层,基于角色的授权基于声明的授权均使用要求、要求处理程序和预配置的策略。 这些构建基块支持代码中的授权评估的表达式。 其结果为一个更丰富、可重用且可测试的授权结构。

授权策略包含一个或多个要求。 在应用的 Program.cs 文件中,将其注册为授权服务配置的一部分:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AtLeast21", policy =>
        policy.Requirements.Add(new MinimumAgeRequirement(21)));
});

在前面的示例中,创建了“AtLeast21”策略。 该策略有一个最低年龄要求,其作为要求的参数提供。

IAuthorizationService

确定授权是否成功的主要服务是 IAuthorizationService

/// <summary>
/// Checks policy based permissions for a user
/// </summary>
public interface IAuthorizationService
{
    /// <summary>
    /// Checks if a user meets a specific set of requirements for the specified resource
    /// </summary>
    /// <param name="user">The user to evaluate the requirements against.</param>
    /// <param name="resource">
    /// An optional resource the policy should be checked with.
    /// If a resource is not required for policy evaluation you may pass null as the value
    /// </param>
    /// <param name="requirements">The requirements to evaluate.</param>
    /// <returns>
    /// A flag indicating whether authorization has succeeded.
    /// This value is <value>true</value> when the user fulfills the policy; 
    /// otherwise <value>false</value>.
    /// </returns>
    /// <remarks>
    /// Resource is an optional parameter and may be null. Please ensure that you check 
    /// it is not null before acting upon it.
    /// </remarks>
    Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, 
                                     IEnumerable<IAuthorizationRequirement> requirements);

    /// <summary>
    /// Checks if a user meets a specific authorization policy
    /// </summary>
    /// <param name="user">The user to check the policy against.</param>
    /// <param name="resource">
    /// An optional resource the policy should be checked with.
    /// If a resource is not required for policy evaluation you may pass null as the value
    /// </param>
    /// <param name="policyName">The name of the policy to check against a specific 
    /// context.</param>
    /// <returns>
    /// A flag indicating whether authorization has succeeded.
    /// Returns a flag indicating whether the user, and optional resource has fulfilled 
    /// the policy.    
    /// <value>true</value> when the policy has been fulfilled; 
    /// otherwise <value>false</value>.
    /// </returns>
    /// <remarks>
    /// Resource is an optional parameter and may be null. Please ensure that you check
    /// it is not null before acting upon it.
    /// </remarks>
    Task<AuthorizationResult> AuthorizeAsync(
                                ClaimsPrincipal user, object resource, string policyName);
}

前面的代码突出显示了 IAuthorizationService 的两种方法。

IAuthorizationRequirement 是一项没有方法的标记服务以及用于跟踪授权是否成功的机制。

每个 IAuthorizationHandler 负责检查是否满足要求:

/// <summary>
/// Classes implementing this interface are able to make a decision if authorization
/// is allowed.
/// </summary>
public interface IAuthorizationHandler
{
    /// <summary>
    /// Makes a decision if authorization is allowed.
    /// </summary>
    /// <param name="context">The authorization information.</param>
    Task HandleAsync(AuthorizationHandlerContext context);
}

AuthorizationHandlerContext 类是处理程序用来标记是否满足要求的类:

 context.Succeed(requirement)

以下代码显示授权服务的简化(并带有注释)默认实现:

public async Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, 
             object resource, IEnumerable<IAuthorizationRequirement> requirements)
{
    // Create a tracking context from the authorization inputs.
    var authContext = _contextFactory.CreateContext(requirements, user, resource);

    // By default this returns an IEnumerable<IAuthorizationHandlers> from DI.
    var handlers = await _handlers.GetHandlersAsync(authContext);

    // Invoke all handlers.
    foreach (var handler in handlers)
    {
        await handler.HandleAsync(authContext);
    }

    // Check the context, by default success is when all requirements have been met.
    return _evaluator.Evaluate(authContext);
}

以下代码显示典型的授权服务配置:

// Add all of your handlers to DI.
builder.Services.AddSingleton<IAuthorizationHandler, MyHandler1>();
// MyHandler2, ...

builder.Services.AddSingleton<IAuthorizationHandler, MyHandlerN>();

// Configure your policies
builder.Services.AddAuthorization(options =>
      options.AddPolicy("Something",
      policy => policy.RequireClaim("Permission", "CanViewPage", "CanViewAnything")));

使用 IAuthorizationService[Authorize(Policy = "Something")]RequireAuthorization("Something") 进行授权。

将策略应用于 MVC 控制器

有关使用 Razor Pages 的应用,请参阅“ 将策略应用于 Razor 页面” 部分。

使用具有策略名称的 [Authorize] 属性将策略应用到控制器。 例如:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace AuthorizationPoliciesSample.Controllers;

[Authorize(Policy = "AtLeast21")]
public class AtLeast21Controller : Controller
{
    public IActionResult Index() => View();
}

将策略应用于 Razor Pages

使用具有策略名称的 [Authorize] 属性将策略应用到 Razor Pages。 例如:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace AuthorizationPoliciesSample.Pages;

[Authorize(Policy = "AtLeast21")]
public class AtLeast21Model : PageModel { }

不能在 Razor Pages 处理程序级别上应用策略,而是必须将其应用于该 Pages。

也可以通过使用授权约定将策略应用于 Razor Pages。

将策略应用到终结点

使用具有策略名称的 RequireAuthorization 将策略应用到终结点。 例如:

app.MapGet("/helloworld", () => "Hello World!")
    .RequireAuthorization("AtLeast21");

要求

授权要求是策略可用于评估当前用户主体的数据参数的集合。 在“AtLeast21”策略中,要求是“最小年龄”参数。 某个要求实现空的标记接口 IAuthorizationRequirement。 可实现参数化的最小年龄要求,如下所示:

using Microsoft.AspNetCore.Authorization;

namespace AuthorizationPoliciesSample.Policies.Requirements;

public class MinimumAgeRequirement : IAuthorizationRequirement
{
    public MinimumAgeRequirement(int minimumAge) =>
        MinimumAge = minimumAge;

    public int MinimumAge { get; }
}

如果授权策略包含多个授权要求,则必须通过所有要求才能使策略评估成功。 也就是说,添加到单个授权策略的多个授权要求将基于 AND 进行处理。

注意

要求不需要具有数据或属性。

授权处理程序

授权处理程序负责评估要求属性。 授权处理程序根据提供的 AuthorizationHandlerContext 评估要求,以确定是否允许访问。

一个要求可以有多个处理程序。 处理程序可以继承 AuthorizationHandler<TRequirement>,其中 TRequirement 是要处理的要求。 处理程序也可直接实现 IAuthorizationHandler 以处理多种类型的要求。

针对一个要求使用处理程序

以下示例显示一对一关系,其中最小年龄处理程序处理单个要求:

using System.Security.Claims;
using AuthorizationPoliciesSample.Policies.Requirements;
using Microsoft.AspNetCore.Authorization;

namespace AuthorizationPoliciesSample.Policies.Handlers;

public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, MinimumAgeRequirement requirement)
    {
        var dateOfBirthClaim = context.User.FindFirst(
            c => c.Type == ClaimTypes.DateOfBirth && c.Issuer == "http://contoso.com");

        if (dateOfBirthClaim is null)
        {
            return Task.CompletedTask;
        }

        var dateOfBirth = Convert.ToDateTime(dateOfBirthClaim.Value);
        int calculatedAge = DateTime.Today.Year - dateOfBirth.Year;
        if (dateOfBirth > DateTime.Today.AddYears(-calculatedAge))
        {
            calculatedAge--;
        }

        if (calculatedAge >= requirement.MinimumAge)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

前面的代码确定当前用户主体是否具有由已知且受信任的颁发者颁发的出生日期声明。 如果缺少声明,则无法进行授权,在这种情况下,将返回已完成的任务。 如果存在声明,将计算用户的年龄。 如果用户满足要求定义的最低年龄,则视为授权成功。 授权成功后,将调用 context.Succeed,并将满足的要求作为其唯一参数。

针对多个要求使用处理程序

以下示例显示一对多关系,其中权限处理程序可以处理三种不同类型的要求:

using System.Security.Claims;
using AuthorizationPoliciesSample.Policies.Requirements;
using Microsoft.AspNetCore.Authorization;

namespace AuthorizationPoliciesSample.Policies.Handlers;

public class PermissionHandler : IAuthorizationHandler
{
    public Task HandleAsync(AuthorizationHandlerContext context)
    {
        var pendingRequirements = context.PendingRequirements.ToList();

        foreach (var requirement in pendingRequirements)
        {
            if (requirement is ReadPermission)
            {
                if (IsOwner(context.User, context.Resource)
                    || IsSponsor(context.User, context.Resource))
                {
                    context.Succeed(requirement);
                }
            }
            else if (requirement is EditPermission || requirement is DeletePermission)
            {
                if (IsOwner(context.User, context.Resource))
                {
                    context.Succeed(requirement);
                }
            }
        }

        return Task.CompletedTask;
    }

    private static bool IsOwner(ClaimsPrincipal user, object? resource)
    {
        // Code omitted for brevity
        return true;
    }

    private static bool IsSponsor(ClaimsPrincipal user, object? resource)
    {
        // Code omitted for brevity
        return true;
    }
}

前面的代码遍历 PendingRequirements - 一个包含未标记为成功的要求的属性。 对于 ReadPermission 要求,用户必须是所有者或发起人才能访问所请求的资源。 对于 EditPermissionDeletePermission 要求,他们必须是所有者才能访问所请求的资源。

处理程序注册

在配置过程中,在服务集合中注册处理程序。 例如:

builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();

上述代码将 MinimumAgeHandler 注册为单一实例。 可使用任何内置的服务生存期来注册处理程序。

可以将要求和处理程序捆绑到一个实现 IAuthorizationRequirementIAuthorizationHandler 的类中。 此捆绑将在处理程序和要求之间建立紧密的耦合,建议仅用于简单的要求和处理程序。 创建实现这两个接口的类无需在 DI 中注册处理程序,因为内置的 PassThroughAuthorizationHandler 允许要求自行处理。

若要了解一个很好的示例,请参阅 AssertionRequirement 类,其中 AssertionRequirement 既是要求又是完全独立的类中的处理程序。

处理程序应会返回哪种结果?

请注意,处理程序示例 中的 Handle 方法不返回任何值。 如何表示成功或失败状态?

  • 处理程序通过调用 context.Succeed(IAuthorizationRequirement requirement) 并传递已成功验证的要求来指示成功。

  • 处理程序通常不需要处理失败,因为针对相同要求的其他处理程序可能会成功。

  • 为了保证失败,即使其他要求处理程序成功,也需调用 context.Fail

如果处理程序调用 context.Succeedcontext.Fail,则仍将调用所有其他处理程序。 这允许要求产生副作用(例如日志记录),即使另一个处理程序已成功验证或要求失败,也会发生这种情况。 当设置为 false 时,InvokeHandlersAfterFailure 属性将在调用 context.Fail 时缩短处理程序的执行时间。 InvokeHandlersAfterFailure 默认为 true,在这种情况下,将调用所有处理程序。

注意

即使身份验证失败,也需调用授权处理程序。

为什么需要多个处理程序才能实现一个要求?

如果想要基于 OR 进行评估,请为单个要求实现多个处理程序。 例如,Microsoft 的门只能用钥匙卡打开。 如果你忘记带钥匙卡,接待员会打印一张临时贴纸并为你开门。 在这种情况下,你有一个要求,即 BuildingEntry,但存在多个处理程序,因此每个处理程序都需要检查该项要求。

BuildingEntryRequirement.cs

using Microsoft.AspNetCore.Authorization;

namespace AuthorizationPoliciesSample.Policies.Requirements;

public class BuildingEntryRequirement : IAuthorizationRequirement { }

BadgeEntryHandler.cs

using AuthorizationPoliciesSample.Policies.Requirements;
using Microsoft.AspNetCore.Authorization;

namespace AuthorizationPoliciesSample.Policies.Handlers;

public class BadgeEntryHandler : AuthorizationHandler<BuildingEntryRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, BuildingEntryRequirement requirement)
    {
        if (context.User.HasClaim(
            c => c.Type == "BadgeId" && c.Issuer == "https://microsoftsecurity"))
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

TemporaryStickerHandler.cs

using AuthorizationPoliciesSample.Policies.Requirements;
using Microsoft.AspNetCore.Authorization;

namespace AuthorizationPoliciesSample.Policies.Handlers;

public class TemporaryStickerHandler : AuthorizationHandler<BuildingEntryRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, BuildingEntryRequirement requirement)
    {
        if (context.User.HasClaim(
            c => c.Type == "TemporaryBadgeId" && c.Issuer == "https://microsoftsecurity"))
        {
            // Code to check expiration date omitted for brevity.
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

确保两个处理程序均已注册。 如果在策略评估 BuildingEntryRequirement 时任一处理程序成功,则策略评估成功。

使用 func 来执行策略

在某些情况下,执行策略非常简单,用代码表示即可。 使用 RequireAssertion 策略生成器配置策略时,可以提供 Func<AuthorizationHandlerContext, bool>

例如,可以重新编写前面的 BadgeEntryHandler,如下所示:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("BadgeEntry", policy =>
        policy.RequireAssertion(context => context.User.HasClaim(c =>
            (c.Type == "BadgeId" || c.Type == "TemporaryBadgeId")
            && c.Issuer == "https://microsoftsecurity")));
});

在处理程序中访问 MVC 请求上下文

HandleRequirementAsync 方法有两个参数:一个 AuthorizationHandlerContext 和正在处理的 TRequirement。 MVC 或 SignalR 等框架可将任何对象任意添加到 AuthorizationHandlerContext 上的 Resource 属性,以传递额外信息。

使用终结点路由时,授权通常由授权中间件处理。 在这种情况下,Resource 属性是 HttpContext 实例。 可使用上下文来访问当前终结点,该终结点可用于探测要路由到的基础资源。 例如:

if (context.Resource is HttpContext httpContext)
{
    var endpoint = httpContext.GetEndpoint();
    var actionDescriptor = endpoint.Metadata.GetMetadata<ControllerActionDescriptor>();
    ...
}

对于传统路由,或者当授权作为 MVC 授权筛选器的一部分发生时,Resource 的值是一个 AuthorizationFilterContext 实例。 此属性提供对 HttpContextRouteData 以及由 MVC 和 Razor Pages 提供的所有其他内容的访问权限。

Resource 属性的用法特定于框架。 使用 Resource 属性中的信息会将授权策略限制为特定框架。 使用 is 关键字强制转换 Resource 属性,然后确认强制转换已成功,以确保代码在其他框架上运行时不会因 InvalidCastException 而故障:

// Requires the following import:
//     using Microsoft.AspNetCore.Mvc.Filters;
if (context.Resource is AuthorizationFilterContext mvcContext)
{
    // Examine MVC-specific things like routing data.
}

全局要求所有用户都需经过身份验证

有关如何全局要求所有用户进行身份验证的信息,请参阅需要通过身份验证的用户

在底层,基于角色的授权基于声明的授权均使用要求、要求处理程序和预配置的策略。 这些构建基块支持代码中的授权评估的表达式。 其结果为一个更丰富、可重用且可测试的授权结构。

授权策略包含一个或多个要求。 该策略在 Startup.ConfigureServices 方法中作为授权服务配置的一部分注册:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    services.AddRazorPages();
    services.AddAuthorization(options =>
    {
        options.AddPolicy("AtLeast21", policy =>
            policy.Requirements.Add(new MinimumAgeRequirement(21)));
    });
}

在前面的示例中,创建了“AtLeast21”策略。 该策略有一个最低年龄要求,其作为要求的参数提供。

IAuthorizationService

确定授权是否成功的主要服务是 IAuthorizationService

/// <summary>
/// Checks policy based permissions for a user
/// </summary>
public interface IAuthorizationService
{
    /// <summary>
    /// Checks if a user meets a specific set of requirements for the specified resource
    /// </summary>
    /// <param name="user">The user to evaluate the requirements against.</param>
    /// <param name="resource">
    /// An optional resource the policy should be checked with.
    /// If a resource is not required for policy evaluation you may pass null as the value
    /// </param>
    /// <param name="requirements">The requirements to evaluate.</param>
    /// <returns>
    /// A flag indicating whether authorization has succeeded.
    /// This value is <value>true</value> when the user fulfills the policy; 
    /// otherwise <value>false</value>.
    /// </returns>
    /// <remarks>
    /// Resource is an optional parameter and may be null. Please ensure that you check 
    /// it is not null before acting upon it.
    /// </remarks>
    Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, 
                                     IEnumerable<IAuthorizationRequirement> requirements);

    /// <summary>
    /// Checks if a user meets a specific authorization policy
    /// </summary>
    /// <param name="user">The user to check the policy against.</param>
    /// <param name="resource">
    /// An optional resource the policy should be checked with.
    /// If a resource is not required for policy evaluation you may pass null as the value
    /// </param>
    /// <param name="policyName">The name of the policy to check against a specific 
    /// context.</param>
    /// <returns>
    /// A flag indicating whether authorization has succeeded.
    /// Returns a flag indicating whether the user, and optional resource has fulfilled 
    /// the policy.    
    /// <value>true</value> when the policy has been fulfilled; 
    /// otherwise <value>false</value>.
    /// </returns>
    /// <remarks>
    /// Resource is an optional parameter and may be null. Please ensure that you check
    /// it is not null before acting upon it.
    /// </remarks>
    Task<AuthorizationResult> AuthorizeAsync(
                                ClaimsPrincipal user, object resource, string policyName);
}

前面的代码突出显示了 IAuthorizationService 的两种方法。

IAuthorizationRequirement 是一项没有方法的标记服务以及用于跟踪授权是否成功的机制。

每个 IAuthorizationHandler 负责检查是否满足要求:

/// <summary>
/// Classes implementing this interface are able to make a decision if authorization
/// is allowed.
/// </summary>
public interface IAuthorizationHandler
{
    /// <summary>
    /// Makes a decision if authorization is allowed.
    /// </summary>
    /// <param name="context">The authorization information.</param>
    Task HandleAsync(AuthorizationHandlerContext context);
}

AuthorizationHandlerContext 类是处理程序用来标记是否满足要求的类:

 context.Succeed(requirement)

以下代码显示授权服务的简化(并带有注释)默认实现:

public async Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, 
             object resource, IEnumerable<IAuthorizationRequirement> requirements)
{
    // Create a tracking context from the authorization inputs.
    var authContext = _contextFactory.CreateContext(requirements, user, resource);

    // By default this returns an IEnumerable<IAuthorizationHandlers> from DI.
    var handlers = await _handlers.GetHandlersAsync(authContext);

    // Invoke all handlers.
    foreach (var handler in handlers)
    {
        await handler.HandleAsync(authContext);
    }

    // Check the context, by default success is when all requirements have been met.
    return _evaluator.Evaluate(authContext);
}

以下代码显示一个典型的 ConfigureServices

public void ConfigureServices(IServiceCollection services)
{
    // Add all of your handlers to DI.
    services.AddSingleton<IAuthorizationHandler, MyHandler1>();
    // MyHandler2, ...

    services.AddSingleton<IAuthorizationHandler, MyHandlerN>();

    // Configure your policies
    services.AddAuthorization(options =>
          options.AddPolicy("Something",
          policy => policy.RequireClaim("Permission", "CanViewPage", "CanViewAnything")));


    services.AddControllersWithViews();
    services.AddRazorPages();
}

使用 IAuthorizationService[Authorize(Policy = "Something")] 进行授权。

将策略应用于 MVC 控制器

如果使用 Razor Pages,请参阅本文档中的“ 将策略应用于 Razor 页面 ”。

策略通过具有策略名称的 [Authorize] 属性应用到控制器。 例如:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

[Authorize(Policy = "AtLeast21")]
public class AlcoholPurchaseController : Controller
{
    public IActionResult Index() => View();
}

将策略应用于 Razor Pages

通过使用具有策略名称的 [Authorize] 属性,将策略应用于 Razor Pages。 例如:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;

[Authorize(Policy = "AtLeast21")]
public class AlcoholPurchaseModel : PageModel
{
}

不能在 Razor Pages 处理程序级别上应用策略,而是必须将其应用于该 Pages。

通过使用授权约定,可以将策略应用于 Razor Pages。

要求

授权要求是策略可用于评估当前用户主体的数据参数的集合。 在“AtLeast21”策略中,要求是“最小年龄”参数。 某个要求实现空的标记接口 IAuthorizationRequirement。 可实现参数化的最小年龄要求,如下所示:

using Microsoft.AspNetCore.Authorization;

public class MinimumAgeRequirement : IAuthorizationRequirement
{
    public int MinimumAge { get; }

    public MinimumAgeRequirement(int minimumAge)
    {
        MinimumAge = minimumAge;
    }
}

如果授权策略包含多个授权要求,则必须通过所有要求才能使策略评估成功。 也就是说,添加到单个授权策略的多个授权要求将基于 AND 进行处理。

注意

要求不需要具有数据或属性。

授权处理程序

授权处理程序负责评估要求属性。 授权处理程序根据提供的 AuthorizationHandlerContext 评估要求,以确定是否允许访问。

一个要求可以有多个处理程序。 处理程序可以继承 AuthorizationHandler<TRequirement>,其中 TRequirement 是要处理的要求。 处理程序也可实现 IAuthorizationHandler 以处理多种类型的要求。

针对一个要求使用处理程序

以下示例显示一对一关系,其中最小年龄处理程序利用单个要求:

using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;

public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
                                                   MinimumAgeRequirement requirement)
    {
        if (!context.User.HasClaim(c => c.Type == ClaimTypes.DateOfBirth &&
                                        c.Issuer == "http://contoso.com"))
        {
            //TODO: Use the following if targeting a version of
            //.NET Framework older than 4.6:
            //      return Task.FromResult(0);
            return Task.CompletedTask;
        }

        var dateOfBirth = Convert.ToDateTime(
            context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth && 
                                        c.Issuer == "http://contoso.com").Value);

        int calculatedAge = DateTime.Today.Year - dateOfBirth.Year;
        if (dateOfBirth > DateTime.Today.AddYears(-calculatedAge))
        {
            calculatedAge--;
        }

        if (calculatedAge >= requirement.MinimumAge)
        {
            context.Succeed(requirement);
        }

        //TODO: Use the following if targeting a version of
        //.NET Framework older than 4.6:
        //      return Task.FromResult(0);
        return Task.CompletedTask;
    }
}

前面的代码确定当前用户主体是否具有由已知且受信任的颁发者颁发的出生日期声明。 如果缺少声明,则无法进行授权,在这种情况下,将返回已完成的任务。 如果存在声明,将计算用户的年龄。 如果用户满足要求定义的最低年龄,则视为授权成功。 授权成功后,将调用 context.Succeed,并将满足的要求作为其唯一参数。

针对多个要求使用处理程序

以下示例显示一对多关系,其中权限处理程序可以处理三种不同类型的要求:

using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;

public class PermissionHandler : IAuthorizationHandler
{
    public Task HandleAsync(AuthorizationHandlerContext context)
    {
        var pendingRequirements = context.PendingRequirements.ToList();

        foreach (var requirement in pendingRequirements)
        {
            if (requirement is ReadPermission)
            {
                if (IsOwner(context.User, context.Resource) ||
                    IsSponsor(context.User, context.Resource))
                {
                    context.Succeed(requirement);
                }
            }
            else if (requirement is EditPermission ||
                     requirement is DeletePermission)
            {
                if (IsOwner(context.User, context.Resource))
                {
                    context.Succeed(requirement);
                }
            }
        }

        //TODO: Use the following if targeting a version of
        //.NET Framework older than 4.6:
        //      return Task.FromResult(0);
        return Task.CompletedTask;
    }

    private bool IsOwner(ClaimsPrincipal user, object resource)
    {
        // Code omitted for brevity

        return true;
    }

    private bool IsSponsor(ClaimsPrincipal user, object resource)
    {
        // Code omitted for brevity

        return true;
    }
}

前面的代码遍历 PendingRequirements - 一个包含未标记为成功的要求的属性。 对于 ReadPermission 要求,用户必须是所有者或发起人才能访问所请求的资源。 对于 EditPermissionDeletePermission 要求,用户必须是所有者才能访问所请求的资源。

处理程序注册

在配置过程中,将在服务集合中注册处理程序。 例如:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    services.AddRazorPages();
    services.AddAuthorization(options =>
    {
        options.AddPolicy("AtLeast21", policy =>
            policy.Requirements.Add(new MinimumAgeRequirement(21)));
    });

    services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
}

上述代码通过调用 services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();MinimumAgeHandler 注册为单一实例。 可使用任何内置的服务生存期来注册处理程序。

可以将要求和处理程序捆绑到一个实现 IAuthorizationRequirementIAuthorizationHandler 的类中。 此捆绑将在处理程序和要求之间建立紧密的耦合,建议仅用于简单的要求和处理程序。 创建实现这两个接口的类无需在 DI 中注册处理程序,因为内置的 PassThroughAuthorizationHandler 允许要求自行处理。

若要了解一个很好的示例,请参阅 AssertionRequirement 类,其中 AssertionRequirement 既是要求又是完全独立的类中的处理程序。

处理程序应会返回哪种结果?

请注意,处理程序示例 中的 Handle 方法不返回任何值。 如何表示成功或失败状态?

  • 处理程序通过调用 context.Succeed(IAuthorizationRequirement requirement) 并传递已成功验证的要求来指示成功。

  • 处理程序通常不需要处理失败,因为针对相同要求的其他处理程序可能会成功。

  • 为了保证失败,即使其他要求处理程序成功,也需调用 context.Fail

如果处理程序调用 context.Succeedcontext.Fail,则仍将调用所有其他处理程序。 这允许要求产生副作用(例如日志记录),即使另一个处理程序已成功验证或要求失败,也会发生这种情况。 当设置为 false 时,InvokeHandlersAfterFailure 属性将在调用 context.Fail 时缩短处理程序的执行时间。 InvokeHandlersAfterFailure 默认为 true,在这种情况下,将调用所有处理程序。

注意

即使身份验证失败,也需调用授权处理程序。

为什么需要多个处理程序才能实现一个要求?

如果想要基于 OR 进行评估,请为单个要求实现多个处理程序。 例如,Microsoft 的门只能用钥匙卡打开。 如果你忘记带钥匙卡,接待员会打印一张临时贴纸并为你开门。 在这种情况下,你有一个要求,即 BuildingEntry,但存在多个处理程序,因此每个处理程序都需要检查该项要求。

BuildingEntryRequirement.cs

using Microsoft.AspNetCore.Authorization;

public class BuildingEntryRequirement : IAuthorizationRequirement
{
}

BadgeEntryHandler.cs

using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;

public class BadgeEntryHandler : AuthorizationHandler<BuildingEntryRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
                                                   BuildingEntryRequirement requirement)
    {
        if (context.User.HasClaim(c => c.Type == "BadgeId" &&
                                       c.Issuer == "http://microsoftsecurity"))
        {
            context.Succeed(requirement);
        }

        //TODO: Use the following if targeting a version of
        //.NET Framework older than 4.6:
        //      return Task.FromResult(0);
        return Task.CompletedTask;
    }
}

TemporaryStickerHandler.cs

using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;

public class TemporaryStickerHandler : AuthorizationHandler<BuildingEntryRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, 
                                                   BuildingEntryRequirement requirement)
    {
        if (context.User.HasClaim(c => c.Type == "TemporaryBadgeId" &&
                                       c.Issuer == "https://microsoftsecurity"))
        {
            // We'd also check the expiration date on the sticker.
            context.Succeed(requirement);
        }

        //TODO: Use the following if targeting a version of
        //.NET Framework older than 4.6:
        //      return Task.FromResult(0);
        return Task.CompletedTask;
    }
}

确保两个处理程序均已注册。 如果在策略评估 BuildingEntryRequirement 时任一处理程序成功,则策略评估成功。

使用 func 来执行策略

在某些情况下,执行策略非常简单,用代码表示即可。 使用 RequireAssertion 策略生成器配置策略时,可以提供 Func<AuthorizationHandlerContext, bool>

例如,可以重新编写前面的 BadgeEntryHandler,如下所示:

services.AddAuthorization(options =>
{
     options.AddPolicy("BadgeEntry", policy =>
        policy.RequireAssertion(context =>
            context.User.HasClaim(c =>
                (c.Type == "BadgeId" ||
                 c.Type == "TemporaryBadgeId") &&
                 c.Issuer == "https://microsoftsecurity")));
});

在处理程序中访问 MVC 请求上下文

在授权处理程序中实现的 HandleRequirementAsync 方法有两个参数:一个 AuthorizationHandlerContext 和正在处理的 TRequirement。 MVC 或 SignalR 等框架可将任何对象任意添加到 AuthorizationHandlerContext 上的 Resource 属性,以传递额外信息。

使用终结点路由时,授权通常由授权中间件处理。 在这种情况下,Resource 属性是 HttpContext 实例。 可使用上下文来访问当前终结点,该终结点可用于探测要路由到的基础资源。 例如:

if (context.Resource is HttpContext httpContext)
{
    var endpoint = httpContext.GetEndpoint();
    var actionDescriptor = endpoint.Metadata.GetMetadata<ControllerActionDescriptor>();
    ...
}

对于传统路由,或者当授权作为 MVC 授权筛选器的一部分发生时,Resource 的值是一个 AuthorizationFilterContext 实例。 此属性提供对 HttpContextRouteData 以及由 MVC 和 Razor Pages 提供的所有其他内容的访问权限。

Resource 属性的用法特定于框架。 使用 Resource 属性中的信息会将授权策略限制为特定框架。 使用 is 关键字强制转换 Resource 属性,然后确认强制转换已成功,以确保代码在其他框架上运行时不会因 InvalidCastException 而故障:

// Requires the following import:
//     using Microsoft.AspNetCore.Mvc.Filters;
if (context.Resource is AuthorizationFilterContext mvcContext)
{
    // Examine MVC-specific things like routing data.
}

全局要求所有用户都需经过身份验证

有关如何全局要求所有用户进行身份验证的信息,请参阅需要通过身份验证的用户

在底层,基于角色的授权基于声明的授权均使用要求、要求处理程序和预配置的策略。 这些构建基块支持代码中的授权评估的表达式。 其结果为一个更丰富、可重用且可测试的授权结构。

授权策略包含一个或多个要求。 该策略在 Startup.ConfigureServices 方法中作为授权服务配置的一部分注册:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    services.AddRazorPages();
    services.AddAuthorization(options =>
    {
        options.AddPolicy("AtLeast21", policy =>
            policy.Requirements.Add(new MinimumAgeRequirement(21)));
    });
}

在前面的示例中,创建了“AtLeast21”策略。 该策略有一个最低年龄要求,其作为要求的参数提供。

IAuthorizationService

确定授权是否成功的主要服务是 IAuthorizationService

/// <summary>
/// Checks policy based permissions for a user
/// </summary>
public interface IAuthorizationService
{
    /// <summary>
    /// Checks if a user meets a specific set of requirements for the specified resource
    /// </summary>
    /// <param name="user">The user to evaluate the requirements against.</param>
    /// <param name="resource">
    /// An optional resource the policy should be checked with.
    /// If a resource is not required for policy evaluation you may pass null as the value
    /// </param>
    /// <param name="requirements">The requirements to evaluate.</param>
    /// <returns>
    /// A flag indicating whether authorization has succeeded.
    /// This value is <value>true</value> when the user fulfills the policy; 
    /// otherwise <value>false</value>.
    /// </returns>
    /// <remarks>
    /// Resource is an optional parameter and may be null. Please ensure that you check 
    /// it is not null before acting upon it.
    /// </remarks>
    Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, 
                                     IEnumerable<IAuthorizationRequirement> requirements);

    /// <summary>
    /// Checks if a user meets a specific authorization policy
    /// </summary>
    /// <param name="user">The user to check the policy against.</param>
    /// <param name="resource">
    /// An optional resource the policy should be checked with.
    /// If a resource is not required for policy evaluation you may pass null as the value
    /// </param>
    /// <param name="policyName">The name of the policy to check against a specific 
    /// context.</param>
    /// <returns>
    /// A flag indicating whether authorization has succeeded.
    /// Returns a flag indicating whether the user, and optional resource has fulfilled 
    /// the policy.    
    /// <value>true</value> when the policy has been fulfilled; 
    /// otherwise <value>false</value>.
    /// </returns>
    /// <remarks>
    /// Resource is an optional parameter and may be null. Please ensure that you check
    /// it is not null before acting upon it.
    /// </remarks>
    Task<AuthorizationResult> AuthorizeAsync(
                                ClaimsPrincipal user, object resource, string policyName);
}

前面的代码突出显示了 IAuthorizationService 的两种方法。

IAuthorizationRequirement 是一项没有方法的标记服务以及用于跟踪授权是否成功的机制。

每个 IAuthorizationHandler 负责检查是否满足要求:

/// <summary>
/// Classes implementing this interface are able to make a decision if authorization
/// is allowed.
/// </summary>
public interface IAuthorizationHandler
{
    /// <summary>
    /// Makes a decision if authorization is allowed.
    /// </summary>
    /// <param name="context">The authorization information.</param>
    Task HandleAsync(AuthorizationHandlerContext context);
}

AuthorizationHandlerContext 类是处理程序用来标记是否满足要求的类:

 context.Succeed(requirement)

以下代码显示授权服务的简化(并带有注释)默认实现:

public async Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, 
             object resource, IEnumerable<IAuthorizationRequirement> requirements)
{
    // Create a tracking context from the authorization inputs.
    var authContext = _contextFactory.CreateContext(requirements, user, resource);

    // By default this returns an IEnumerable<IAuthorizationHandlers> from DI.
    var handlers = await _handlers.GetHandlersAsync(authContext);

    // Invoke all handlers.
    foreach (var handler in handlers)
    {
        await handler.HandleAsync(authContext);
    }

    // Check the context, by default success is when all requirements have been met.
    return _evaluator.Evaluate(authContext);
}

以下代码显示一个典型的 ConfigureServices

public void ConfigureServices(IServiceCollection services)
{
    // Add all of your handlers to DI.
    services.AddSingleton<IAuthorizationHandler, MyHandler1>();
    // MyHandler2, ...

    services.AddSingleton<IAuthorizationHandler, MyHandlerN>();

    // Configure your policies
    services.AddAuthorization(options =>
          options.AddPolicy("Something",
          policy => policy.RequireClaim("Permission", "CanViewPage", "CanViewAnything")));


    services.AddControllersWithViews();
    services.AddRazorPages();
}

使用 IAuthorizationService[Authorize(Policy = "Something")] 进行授权。

将策略应用于 MVC 控制器

如果使用 Razor Pages,请参阅本文档中的 “将策略应用于 Razor 页面 ”。

策略通过具有策略名称的 [Authorize] 属性应用到控制器。 例如:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

[Authorize(Policy = "AtLeast21")]
public class AlcoholPurchaseController : Controller
{
    public IActionResult Index() => View();
}

将策略应用于 Razor Pages

通过使用具有策略名称的 [Authorize] 属性,将策略应用于 Razor Pages。 例如:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;

[Authorize(Policy = "AtLeast21")]
public class AlcoholPurchaseModel : PageModel
{
}

不能在 Razor Pages 处理程序级别上应用策略,而是必须将其应用于该 Pages。

通过使用授权约定,可以将策略应用于 Razor Pages。

要求

授权要求是策略可用于评估当前用户主体的数据参数的集合。 在“AtLeast21”策略中,要求是“最小年龄”参数。 某个要求实现空的标记接口 IAuthorizationRequirement。 可实现参数化的最小年龄要求,如下所示:

using Microsoft.AspNetCore.Authorization;

public class MinimumAgeRequirement : IAuthorizationRequirement
{
    public int MinimumAge { get; }

    public MinimumAgeRequirement(int minimumAge)
    {
        MinimumAge = minimumAge;
    }
}

如果授权策略包含多个授权要求,则必须通过所有要求才能使策略评估成功。 也就是说,添加到单个授权策略的多个授权要求将基于 AND 进行处理。

注意

要求不需要具有数据或属性。

授权处理程序

授权处理程序负责评估要求属性。 授权处理程序根据提供的 AuthorizationHandlerContext 评估要求,以确定是否允许访问。

一个要求可以有多个处理程序。 处理程序可以继承 AuthorizationHandler<TRequirement>,其中 TRequirement 是要处理的要求。 处理程序也可实现 IAuthorizationHandler 以处理多种类型的要求。

针对一个要求使用处理程序

以下示例显示一对一关系,其中最小年龄处理程序利用单个要求:

using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;

public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
                                                   MinimumAgeRequirement requirement)
    {
        if (!context.User.HasClaim(c => c.Type == ClaimTypes.DateOfBirth &&
                                        c.Issuer == "http://contoso.com"))
        {
            //TODO: Use the following if targeting a version of
            //.NET Framework older than 4.6:
            //      return Task.FromResult(0);
            return Task.CompletedTask;
        }

        var dateOfBirth = Convert.ToDateTime(
            context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth && 
                                        c.Issuer == "http://contoso.com").Value);

        int calculatedAge = DateTime.Today.Year - dateOfBirth.Year;
        if (dateOfBirth > DateTime.Today.AddYears(-calculatedAge))
        {
            calculatedAge--;
        }

        if (calculatedAge >= requirement.MinimumAge)
        {
            context.Succeed(requirement);
        }

        //TODO: Use the following if targeting a version of
        //.NET Framework older than 4.6:
        //      return Task.FromResult(0);
        return Task.CompletedTask;
    }
}

前面的代码确定当前用户主体是否具有由已知且受信任的颁发者颁发的出生日期声明。 如果缺少声明,则无法进行授权,在这种情况下,将返回已完成的任务。 如果存在声明,将计算用户的年龄。 如果用户满足要求定义的最低年龄,则视为授权成功。 授权成功后,将调用 context.Succeed,并将满足的要求作为其唯一参数。

针对多个要求使用处理程序

以下示例显示一对多关系,其中权限处理程序可以处理三种不同类型的要求:

using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;

public class PermissionHandler : IAuthorizationHandler
{
    public Task HandleAsync(AuthorizationHandlerContext context)
    {
        var pendingRequirements = context.PendingRequirements.ToList();

        foreach (var requirement in pendingRequirements)
        {
            if (requirement is ReadPermission)
            {
                if (IsOwner(context.User, context.Resource) ||
                    IsSponsor(context.User, context.Resource))
                {
                    context.Succeed(requirement);
                }
            }
            else if (requirement is EditPermission ||
                     requirement is DeletePermission)
            {
                if (IsOwner(context.User, context.Resource))
                {
                    context.Succeed(requirement);
                }
            }
        }

        //TODO: Use the following if targeting a version of
        //.NET Framework older than 4.6:
        //      return Task.FromResult(0);
        return Task.CompletedTask;
    }

    private bool IsOwner(ClaimsPrincipal user, object resource)
    {
        // Code omitted for brevity

        return true;
    }

    private bool IsSponsor(ClaimsPrincipal user, object resource)
    {
        // Code omitted for brevity

        return true;
    }
}

前面的代码遍历 PendingRequirements - 一个包含未标记为成功的要求的属性。 对于 ReadPermission 要求,用户必须是所有者或发起人才能访问所请求的资源。 对于 EditPermissionDeletePermission 要求,用户必须是所有者才能访问所请求的资源。

处理程序注册

在配置过程中,将在服务集合中注册处理程序。 例如:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    services.AddRazorPages();
    services.AddAuthorization(options =>
    {
        options.AddPolicy("AtLeast21", policy =>
            policy.Requirements.Add(new MinimumAgeRequirement(21)));
    });

    services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
}

上述代码通过调用 services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();MinimumAgeHandler 注册为单一实例。 可使用任何内置的服务生存期来注册处理程序。

处理程序应会返回哪种结果?

请注意,处理程序示例 中的 Handle 方法不返回任何值。 如何表示成功或失败状态?

  • 处理程序通过调用 context.Succeed(IAuthorizationRequirement requirement) 并传递已成功验证的要求来指示成功。

  • 处理程序通常不需要处理失败,因为针对相同要求的其他处理程序可能会成功。

  • 为了保证失败,即使其他要求处理程序成功,也需调用 context.Fail

如果处理程序调用 context.Succeedcontext.Fail,则仍将调用所有其他处理程序。 这允许要求产生副作用(例如日志记录),即使另一个处理程序已成功验证或要求失败,也会发生这种情况。 当设置为 false 时,InvokeHandlersAfterFailure 属性将在调用 context.Fail 时缩短处理程序的执行时间。 InvokeHandlersAfterFailure 默认为 true,在这种情况下,将调用所有处理程序。

注意

即使身份验证失败,也需调用授权处理程序。

为什么需要多个处理程序才能实现一个要求?

如果想要基于 OR 进行评估,请为单个要求实现多个处理程序。 例如,Microsoft 的门只能用钥匙卡打开。 如果你忘记带钥匙卡,接待员会打印一张临时贴纸并为你开门。 在这种情况下,你有一个要求,即 BuildingEntry,但存在多个处理程序,因此每个处理程序都需要检查该项要求。

BuildingEntryRequirement.cs

using Microsoft.AspNetCore.Authorization;

public class BuildingEntryRequirement : IAuthorizationRequirement
{
}

BadgeEntryHandler.cs

using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;

public class BadgeEntryHandler : AuthorizationHandler<BuildingEntryRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
                                                   BuildingEntryRequirement requirement)
    {
        if (context.User.HasClaim(c => c.Type == "BadgeId" &&
                                       c.Issuer == "http://microsoftsecurity"))
        {
            context.Succeed(requirement);
        }

        //TODO: Use the following if targeting a version of
        //.NET Framework older than 4.6:
        //      return Task.FromResult(0);
        return Task.CompletedTask;
    }
}

TemporaryStickerHandler.cs

using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;

public class TemporaryStickerHandler : AuthorizationHandler<BuildingEntryRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, 
                                                   BuildingEntryRequirement requirement)
    {
        if (context.User.HasClaim(c => c.Type == "TemporaryBadgeId" &&
                                       c.Issuer == "https://microsoftsecurity"))
        {
            // We'd also check the expiration date on the sticker.
            context.Succeed(requirement);
        }

        //TODO: Use the following if targeting a version of
        //.NET Framework older than 4.6:
        //      return Task.FromResult(0);
        return Task.CompletedTask;
    }
}

确保两个处理程序均已注册。 如果在策略评估 BuildingEntryRequirement 时任一处理程序成功,则策略评估成功。

使用 func 来执行策略

在某些情况下,执行策略非常简单,用代码表示即可。 使用 RequireAssertion 策略生成器配置策略时,可以提供 Func<AuthorizationHandlerContext, bool>

例如,可以重新编写前面的 BadgeEntryHandler,如下所示:

services.AddAuthorization(options =>
{
     options.AddPolicy("BadgeEntry", policy =>
        policy.RequireAssertion(context =>
            context.User.HasClaim(c =>
                (c.Type == "BadgeId" ||
                 c.Type == "TemporaryBadgeId") &&
                 c.Issuer == "https://microsoftsecurity")));
});

在处理程序中访问 MVC 请求上下文

在授权处理程序中实现的 HandleRequirementAsync 方法有两个参数:一个 AuthorizationHandlerContext 和正在处理的 TRequirement。 MVC 或 SignalR 等框架可将任何对象任意添加到 AuthorizationHandlerContext 上的 Resource 属性,以传递额外信息。

使用终结点路由时,授权通常由授权中间件处理。 在这种情况下,Resource 属性是 Endpoint 实例。 终结点可用于探测要路由到的基础资源。 例如:

if (context.Resource is Endpoint endpoint)
{
   var actionDescriptor = endpoint.Metadata.GetMetadata<ControllerActionDescriptor>();
   ...
}

终结点不提供对当前 HttpContext 的访问权限。 当使用终结点路由时,请使用 IHttpContextAccessor 访问授权处理程序内的 HttpContext。 有关详细信息,请参阅使用自定义组件中的 HttpContext

对于传统路由,或者当授权作为 MVC 授权筛选器的一部分发生时,Resource 的值是一个 AuthorizationFilterContext 实例。 此属性提供对 HttpContextRouteData 以及由 MVC 和 Razor Pages 提供的所有其他内容的访问权限。

Resource 属性的用法特定于框架。 使用 Resource 属性中的信息会将授权策略限制为特定框架。 使用 is 关键字强制转换 Resource 属性,然后确认强制转换已成功,以确保代码在其他框架上运行时不会因 InvalidCastException 而故障:

// Requires the following import:
//     using Microsoft.AspNetCore.Mvc.Filters;
if (context.Resource is AuthorizationFilterContext mvcContext)
{
    // Examine MVC-specific things like routing data.
}

全局要求所有用户都需经过身份验证

有关如何全局要求所有用户进行身份验证的信息,请参阅需要通过身份验证的用户